Skip to content

Add direct repository package specs for GitHub, GitLab, Bitbucket, and SourceHut - #38

Merged
adelrodriguez merged 1 commit into
mainfrom
08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut
Aug 12, 2026
Merged

Add direct repository package specs for GitHub, GitLab, Bitbucket, and SourceHut#38
adelrodriguez merged 1 commit into
mainfrom
08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut

Conversation

@adelrodriguez

@adelrodriguez adelrodriguez commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Packref can now fetch package source directly from repository hosts without going through a registry. Passing a repository spec to packref add resolves the ref, pins the commit, and records the entry as a manual reference in the lockfile.

Supported spec formats:

  • GitHub shorthand: owner/repository[/directory][@ref]
  • SourceHut shorthand: ~owner/repository[/directory][@ref]
  • Provider shorthand: github:owner/repository[/directory][@ref], gitlab:…, bitbucket:…, or sourcehut:…
  • Host-prefixed shorthand: github.com/owner/repository[/directory][@ref], gitlab.com/owner/repository[/directory][@ref], bitbucket.org/owner/repository[/directory][@ref], or git.sr.ht/~owner/repository[/directory][@ref]
  • Standard Git URL: https://github.com/owner/repository.git[@ref] or git+https://…
  • SCP-style SSH URL: git@github.com:owner/repository.git[@ref]

The optional ref can be a tag, branch, or full 40-character commit SHA. Packref stores the full resolved commit SHA so add and reinstall use the same immutable ref. Explicit commit SHAs are normalized to lowercase. The requested tag or branch stays in the project lockfile so removal accepts the original package spec without leaking request metadata into the shared store.

ParsedPackageSpec is a discriminated union of RegistryPackageSpec (_tag: "registry") and RepositoryPackageSpec (_tag: "repository"). Registry resolution and removal branch on this distinction. Repository ref discovery requests only HEAD, refs/heads/*, and refs/tags/*, which excludes pull-request and merge-request refs. On reinstall, direct repository entries use their pinned version as the exact fetch ref instead of re-resolving through tag matching.

The global store supports two-segment repository names such as owner/repo. Repository URL, host-prefixed, and provider shorthand forms handle monorepo subdirectories consistently. Direct repository snapshots exclude project-specific directory metadata, so different projects can select different directories while they reuse one full-repository snapshot. Within one project, selecting a second directory for the same repository and commit fails before fetching with a dedicated conflict error that tells the user to remove the existing reference first.

Summary by CodeRabbit

  • New Features

    • Added support for installing packages directly from GitHub, GitLab, Bitbucket, and SourceHut repositories.
    • Supports repository URLs and shorthand forms, optional directories, and pinned branches, tags, commits, or other references.
    • Direct repository sources are tracked and installed using their resolved references.
  • Documentation

    • Updated usage documentation with direct repository source formats and reference-pinning behavior.
  • Bug Fixes

    • Improved handling of repository references, including default branches, tags, annotated tags, and commit identifiers.
    • Improved package-name validation for reserved path segments.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds direct GitHub, GitLab, Bitbucket, and SourceHut package specifications. It parses and resolves repository refs, installs pinned snapshots, records manual lockfile entries, updates registry types, simplifies workspace paths, and revises command documentation.

Changes

Direct repository package support

Layer / File(s) Summary
Package specification parsing
src/lib/core/packages.ts, src/lib/core/__tests__/packages.test.ts
Package parsing now returns tagged registry or repository specifications. Repository inputs support provider prefixes, URLs, SSH forms, shorthand forms, directories, and specifiers.
Repository reference resolution
src/lib/sources/repository/*, src/lib/sources/repository/__tests__/tags.test.ts
Git remote output now includes HEADs, branches, and tags. Direct repository resolution converts these refs or commit-like specifiers into pinned repository references.
Reference materialization and installation
src/lib/references/add.ts, src/lib/references/install.ts, src/lib/references/__tests__/install.test.ts
The add flow resolves and fetches direct repositories, creates project references, records manual lockfile entries, and passes pinned refs to downloads.
Registry type integration
src/lib/registries/*, src/lib/registries/npm/__tests__/resolver.test.ts, src/lib/references/__tests__/remove.test.ts
Registry adapters and selectors now use the tagged RegistryPackageSpec type.
Workspace paths and command behavior
src/lib/workspace/*, src/lib/references/{clean,remove,sync}.ts, src/lib/commands/add.ts, src/commands/remove.ts, README.md, src/lib/workspace/integration.ts, .changeset/bright-repos-arrive.md
Workspace paths use shared constants directly. Remove selection uses undefined for empty selections. CLI and workspace documentation describe direct repository inputs and ref pinning.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant addPackageReference
  participant resolveDirectRepositoryRef
  participant GitRepository
  participant Lockfile
  User->>addPackageReference: provide repository package specification
  addPackageReference->>resolveDirectRepositoryRef: resolve repository and ref
  resolveDirectRepositoryRef->>GitRepository: list HEADs, branches, and tags
  GitRepository-->>resolveDirectRepositoryRef: remote repository refs
  resolveDirectRepositoryRef-->>addPackageReference: normalized source and pinned ref
  addPackageReference->>Lockfile: record manual repository entry
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding direct repository package specifications for the four supported providers.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut

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.

adelrodriguez commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

@adelrodriguez
adelrodriguez changed the base branch from 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects to graphite-base/38 August 11, 2026 15:15
@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from 3ccdec1 to 34ce2a8 Compare August 11, 2026 16:08
@adelrodriguez
adelrodriguez changed the base branch from graphite-base/38 to 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects August 11, 2026 16:09
Base automatically changed from 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects to main August 11, 2026 16:52
@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from 34ce2a8 to 06af555 Compare August 11, 2026 16:55
@adelrodriguez
adelrodriguez marked this pull request as ready for review August 11, 2026 16:55

@pullfrog pullfrog 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.

Important

The feature works and the npm path is intact, but repository specs that Packref cannot serve fail with errors that name the wrong subsystem, and the new add path has no test behind it.

Reviewed changes — full read of the 28-file diff for commit 06af555, checked against docs/plans/11-direct-repository-sources.md, with the spec parser exercised directly and the provider archive endpoints verified live.

  • Tagged spec unionParsedPackageSpec becomes RegistryPackageSpec | RepositoryPackageSpec, and resolvePackageReference / RegistryAdapter.resolve are narrowed to the registry variant so repository specs can never reach a registry adapter.
  • Repository spec detectioncheckIsRepositorySpec and parseRepositoryIdentity in src/lib/core/packages.ts recognize provider shorthands, bare owner/repo, standard URLs, and SCP-style SSH URLs, falling through to the existing registry parsing when no supported provider is derived.
  • Ref pinning — new resolveDirectRepositoryRef pins the default branch and branch refs to a 12-hex SHA, slash-free tags to the tag name, slash-containing tags to the full SHA, and SHA-shaped input verbatim.
  • ls-remote widenedgit ls-remote --tags becomes git ls-remote so HEAD and branches are visible, with a new parseGitRemoteRefsOutput; the pre-existing parseGitRemoteTagsOutput filters on refs/tags/, so the registry-derived tag path is unaffected.
  • Reinstall path — direct repository entries use entry.version as the exact fetch ref instead of re-resolving through tag matching.
  • Store identitygetPackageIdentitySegments now splits any name containing /, producing packages/<provider>/<owner>/<repo>/<version>.
  • Unrelated refactor — the path helpers in workspace/paths.ts and src/lib/shared/path.ts are removed and inlined, and src/commands/remove.ts is rewritten off Option.

Two things I checked that came back clean, so nobody needs to redo them. The plan left "verify giget/codeload accepts abbreviated SHAs; fall back to the full SHA if not" (lines 54-56) as an open item — a 12-hex SHA resolves on all four provider archive endpoints, confirmed against live URLs with ffffffffffff negative controls returning 404, so no full-SHA fallback is needed. And I could not produce any npm spec that the new pre-registry branch misroutes: react@, @effect/cli@0.29.0, npm: react, npm:@effect/cli@0.29.0, lodash.get@4.4.2 all still parse as registry specs, and jsr:/pypi: still raise UnsupportedRegistryError. bun test passes 289/289 and bunx tsc --noEmit is clean.

⚠️ The direct-repository add path has no test behind it

resolveDirectRepositoryRef is unit-tested in tags.test.ts and the reinstall branch is tested in install.test.ts, but addDirectRepositoryReferenceToProject — the function that joins them and writes the lockfile entry — is never executed by a test. Nothing pins the entry shape (registry: "github", name: "owner/repo", tracking: "manual") or the packages/github/<owner>/<repo>/<version> store layout, which are the parts of the identity model most likely to drift.

Technical details
# Cover the direct repository add path

## Affected sites
- `src/lib/references/add.ts:177-204``addDirectRepositoryReferenceToProject` has no test exercising it
- `src/lib/references/__tests__/add.test.ts``addPackageReference` tests only pass `_tag: "registry"` specs
- `docs/plans/11-direct-repository-sources.md:112-117` — plan step 7 asks for an add-side test in `src/lib/references/__tests__/`

## Required outcome
- A test drives `addPackageReference` with a `_tag: "repository"` spec and asserts the resulting lockfile entry's `registry`, `name`, `version`, `tracking`, and `source`, plus the materialized reference path.
- The assertions pin exact values rather than existence, so a regression in the identity model fails the test.

## Suggested approach (optional)
- `add.test.ts` already builds a layer with a fake `RepositoryDownloader`; it needs a `RemoteTagReader` stub too. `tags.test.ts` has `runWithRemoteTagCommand`, which stubs `ls-remote` stdout — the same canned ref output would work here.

## Open questions for the human
- The plan also asks for a network-backed integration test against a real repository. Is that wanted in CI, or is a fully stubbed test the intended scope?

ℹ️ The path-helper removal and remove.ts rewrite are unrelated to this feature

Roughly a third of the diff is refactoring that direct repository specs do not require: four helpers deleted from workspace/paths.ts and inlined at ~12 call sites, src/lib/shared/path.ts deleted with checkIsPathWithin relocated, and src/commands/remove.ts converted from Option/Array.match to undefined/ternaries. The lockfile path path.join(projectPath, PACKREF_DIRECTORY_NAME, LOCKFILE_NAME) is now spelled out in five places instead of one. This is a deliberate-looking direction rather than an accident, so the question is scope, not correctness.

Technical details
# Unrelated refactoring bundled with the feature

## Affected sites
- `src/lib/workspace/paths.ts:8-18``getDirectoryPath`, `getProjectLockfilePath`, `getGlobalDirectoryPath`, `getGlobalConfigPath` removed
- `src/lib/workspace/lockfile.ts:100,114,135,158` and `src/lib/references/clean.ts:32,38` — lockfile path now rebuilt inline at each site
- `src/lib/shared/path.ts` — deleted; `checkIsPathWithin` moved to `workspace/paths.ts`
- `src/commands/remove.ts:45-77,117``Option`-based selection rewritten to `undefined` and ternaries

## Required outcome
- Either split this refactor into its own commit or PR so the feature diff stays reviewable, or confirm it is an intentional codebase-wide direction so reviewers stop flagging it.

## Open questions for the human
- Is moving away from `Option` for internal absent-value plumbing a general convention now, or local to `remove.ts`?
- Was collapsing the named path helpers deliberate? Re-deriving the lockfile path at five call sites makes a future change to the on-disk layout a multi-site edit.

ℹ️ Nitpicks

  • src/lib/core/packages.ts:44-49,98-103SUPPORTED_REPOSITORY_PROVIDERS and REPOSITORY_PROVIDER_HOSTS exactly duplicate KNOWN_PROVIDERS and PROVIDER_HOSTS at src/lib/sources/repository/normalize.ts:17,23-28. Adding a fifth provider now means editing two tables that must agree, or the parser and the normalizer disagree about which hosts are supported.
  • src/lib/sources/repository/normalize.ts:171-173 — a slash-containing tag pins the full 40-hex SHA while HEAD and branch refs pin 12 hex (:166, :181), so PackageEntry.version and the store directory name vary in width for no functional reason.
  • src/lib/core/packages.ts:145,161RegExp.exec returns null, not undefined, so scpMatch !== undefined at :161 is always true once reached and the `${owner}/${repository}` fallback at :164 is unreachable. Harmless today because the SCP branch happens to produce the same string, but it will not stay that way.
  • src/lib/core/packages.ts:171value.slice(0, value.indexOf(":")) becomes slice(0, -1) when there is no colon, so githubs and github@ satisfy checkIsRepositoryProvider and enter the repository branch. They only escape because the derived name ends in / and hits the fallthrough guard.
  • src/lib/core/packages.ts:174 — the trailing (?:@[^@]+)?$ requires at least one character after @, so owner/repo@ misses the gate entirely and is handed to npm as a package named owner/repo, while github:owner/repo@ correctly parses as a repository spec at the default branch. Worth accepting an empty trailing ref for consistency with react@.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/core/packages.ts
Comment thread src/lib/core/packages.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
src/lib/sources/repository/tags.ts (1)

160-160: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

git ls-remote without filters returns every ref.

The command now returns all refs, including refs/pull/* and refs/merge-requests/*. Large repositories publish tens of thousands of such refs, so each lookup transfers and parses much more data than before. HEAD is required, so --heads --tags alone is not sufficient. Consider git ls-remote --symref <url> HEAD "refs/heads/*" "refs/tags/*" to keep HEAD while excluding unrelated ref namespaces.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sources/repository/tags.ts` at line 160, Update the git invocation in
the repository tag lookup to pass --symref, HEAD, and explicit refs/heads/* and
refs/tags/* patterns to ls-remote, preserving HEAD resolution while excluding
pull-request, merge-request, and other unrelated namespaces.
src/lib/core/packages.ts (1)

170-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

checkIsRepositorySpec accepts registry-prefixed specs.

The SCP pattern on line 176 matches npm:react@19.0.0 and npm:@effect/cli@0.29.0, because [^:]+:.+ matches any prefixed spec. Those inputs enter the repository branch and only return to registry parsing because parseRepositoryIdentity produces an unsupported host or a name that ends with /. The correct result depends on a fallback, not on the check. Exclude known registry prefixes from the SCP branch to make the intent explicit.

♻️ Proposed refinement
 const checkIsRepositorySpec = (value: string) => {
   const prefix = value.slice(0, value.indexOf(":"))
+
+  if (checkIsRegistry(prefix)) {
+    return false
+  }
+
   return (
     checkIsRepositoryProvider(prefix) ||
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/core/packages.ts` around lines 170 - 178, Update
checkIsRepositorySpec so its SCP-style pattern excludes known registry prefixes
such as npm before evaluating the generic host:path match. Preserve
repository-provider, repository shorthand, and URL detection while preventing
registry-prefixed specs like npm:react@19.0.0 from entering the repository
branch.
src/lib/core/__tests__/packages.test.ts (1)

346-397: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for the unresolved parsing edges.

The table covers GitHub and GitLab happy paths only. Add cases for github.com/owner/repo, an unsupported host such as https://git.example.com/owner/repo, and a SourceHut spec such as sourcehut:~owner/repo. Those inputs exercise the host default on line 146 of src/lib/core/packages.ts and the fall-through on lines 258-274, so the tests would pin the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/core/__tests__/packages.test.ts` around lines 346 - 397, Extend the
“parses direct repository spec” table in the parsePackageSpec tests with cases
for github.com/owner/repo, an unsupported HTTPS host such as
https://git.example.com/owner/repo, and sourcehut:~owner/repo. Set each expected
result to the intended behavior of the host-default and unsupported/sourcehut
parsing branches, preserving the existing test structure.
src/lib/references/add.ts (1)

177-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared materialization tail.

Lines 181-202 repeat lines 152-173 of materializePackageReferenceToProject: create the project reference, build the entry, upsert it, and build the AddPackageResult. Only the store-entry acquisition differs. Extract the tail into one helper that takes the identity, the store entry, the manifest range, and the tracking mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/references/add.ts` around lines 177 - 204, Extract the duplicated
materialization tail from materializePackageReferenceToProject and
addDirectRepositoryReferenceToProject into a shared helper. Have the helper
accept the package identity, store entry, manifest range, and tracking mode,
then createProjectReference, build and upsert the PackageEntry, and return the
AddPackageResult; replace both inline tails while preserving their existing
inputs and results.
src/lib/sources/repository/normalize.ts (1)

162-191: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the Match predicates.

Each handler repeats ?? "" because the matched value keeps the optional types. Narrow once before the match, or destructure into locals, so the fallbacks disappear. The current form hides which branch can really produce an empty ref.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sources/repository/normalize.ts` around lines 162 - 191, Simplify the
Match flow around the resolved value by narrowing or destructuring optional
fields before matching, so each matched handler receives non-optional values
where its predicate guarantees presence. Remove the redundant ?? "" fallbacks
from the ref and version construction while preserving the existing branch
selection and TagNotFoundError behavior.
src/lib/references/__tests__/install.test.ts (1)

215-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case with a sha-shaped pinned version.

The entry uses 1.0.0, which is a tag-shaped version. Direct repository entries created from HEAD or from a branch store a 12-character sha instead. Add a second case with a sha-shaped version so the test covers the ref that resolveDirectRepositoryRef actually writes to the lockfile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/references/__tests__/install.test.ts` around lines 215 - 234, The
reinstall test currently covers only a tag-shaped pinned ref; add a second test
case alongside “reinstalls a direct repository entry with its exact pinned ref”
using a 12-character SHA-shaped version, initialize and install the entry with
the existing helpers, and assert repositoryRefs contains that exact SHA to cover
resolveDirectRepositoryRef’s lockfile behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/references/install.ts`:
- Around line 60-68: Update the direct-repository branch in the install
resolution flow to obtain an exact commit ref rather than using entry.version,
and preserve the source.fetchSource === undefined validation performed by
resolveRepositoryRef. Reuse the repository normalization/resolution logic
associated with resolveDirectRepositoryRef and resolveRepositoryRef so
unsupported hosts still produce UnsupportedRepositoryHostError instead of
SnapshotFetchError.

In `@src/lib/references/remove.ts`:
- Line 73: Use the project-local .packref/packages root consistently in both
workflows: update projectDirectoryPath in src/lib/references/remove.ts at lines
73-73 and the corresponding path construction in src/lib/references/sync.ts at
lines 84-84 to include the packages directory, so getStorePackagePath resolves
the actual source tree for removal and synchronization.

In `@src/lib/sources/repository/__tests__/tags.test.ts`:
- Around line 292-316: Update resolveDirectRepositoryRef to normalize resolved
commit versions consistently, using the intended truncated SHA format for HEAD,
branch, nested-tag, and abbreviated-commit resolutions. Adjust the affected
expectedVersion values in the pins test table so every case reflects the same
stored version shape while preserving each expected repository.ref.

In `@src/lib/sources/repository/normalize.ts`:
- Around line 162-191: Update resolveDirectRepositoryRef in
src/lib/sources/repository/normalize.ts (lines 162-191) so every resolved
repository case stores the full resolved commit SHA in version, while retaining
tag names only as separate display metadata. In src/lib/references/install.ts
(lines 60-68), use the lockfile’s full pinned SHA as the download ref and
preserve the existing fetchSource === undefined bypass check. Update the
expected versions for the HEAD, branch, and tag cases in
src/lib/sources/repository/__tests__/tags.test.ts (lines 292-316) to the full
SHA.

In `@src/lib/workspace/paths.ts`:
- Around line 8-16: Update materializeStoreEntry and the checkIsPathWithin
validation flow to reject symlinked source paths before invoking
Reflinker.reflink. Resolve the candidate’s real path or inspect each path
component, then validate the resolved path remains within storePath; preserve
the existing lexical boundary checks for non-symlink paths.

---

Nitpick comments:
In `@src/lib/core/__tests__/packages.test.ts`:
- Around line 346-397: Extend the “parses direct repository spec” table in the
parsePackageSpec tests with cases for github.com/owner/repo, an unsupported
HTTPS host such as https://git.example.com/owner/repo, and
sourcehut:~owner/repo. Set each expected result to the intended behavior of the
host-default and unsupported/sourcehut parsing branches, preserving the existing
test structure.

In `@src/lib/core/packages.ts`:
- Around line 170-178: Update checkIsRepositorySpec so its SCP-style pattern
excludes known registry prefixes such as npm before evaluating the generic
host:path match. Preserve repository-provider, repository shorthand, and URL
detection while preventing registry-prefixed specs like npm:react@19.0.0 from
entering the repository branch.

In `@src/lib/references/__tests__/install.test.ts`:
- Around line 215-234: The reinstall test currently covers only a tag-shaped
pinned ref; add a second test case alongside “reinstalls a direct repository
entry with its exact pinned ref” using a 12-character SHA-shaped version,
initialize and install the entry with the existing helpers, and assert
repositoryRefs contains that exact SHA to cover resolveDirectRepositoryRef’s
lockfile behavior.

In `@src/lib/references/add.ts`:
- Around line 177-204: Extract the duplicated materialization tail from
materializePackageReferenceToProject and addDirectRepositoryReferenceToProject
into a shared helper. Have the helper accept the package identity, store entry,
manifest range, and tracking mode, then createProjectReference, build and upsert
the PackageEntry, and return the AddPackageResult; replace both inline tails
while preserving their existing inputs and results.

In `@src/lib/sources/repository/normalize.ts`:
- Around line 162-191: Simplify the Match flow around the resolved value by
narrowing or destructuring optional fields before matching, so each matched
handler receives non-optional values where its predicate guarantees presence.
Remove the redundant ?? "" fallbacks from the ref and version construction while
preserving the existing branch selection and TagNotFoundError behavior.

In `@src/lib/sources/repository/tags.ts`:
- Line 160: Update the git invocation in the repository tag lookup to pass
--symref, HEAD, and explicit refs/heads/* and refs/tags/* patterns to ls-remote,
preserving HEAD resolution while excluding pull-request, merge-request, and
other unrelated namespaces.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 608eb434-798c-4f9a-84cd-6ba9378d1bce

📥 Commits

Reviewing files that changed from the base of the PR and between de6e0cb and 06af555.

📒 Files selected for processing (28)
  • .changeset/bright-repos-arrive.md
  • README.md
  • src/commands/add.ts
  • src/commands/remove.ts
  • src/lib/core/__tests__/packages.test.ts
  • src/lib/core/packages.ts
  • src/lib/references/__tests__/install.test.ts
  • src/lib/references/__tests__/remove.test.ts
  • src/lib/references/add.ts
  • src/lib/references/clean.ts
  • src/lib/references/install.ts
  • src/lib/references/remove.ts
  • src/lib/references/sync.ts
  • src/lib/registries/index.ts
  • src/lib/registries/npm/__tests__/resolver.test.ts
  • src/lib/registries/registry.ts
  • src/lib/shared/path.ts
  • src/lib/sources/repository/__tests__/tags.test.ts
  • src/lib/sources/repository/normalize.ts
  • src/lib/sources/repository/tags.ts
  • src/lib/sources/tarball/fetch.ts
  • src/lib/store/paths.ts
  • src/lib/workspace/config.ts
  • src/lib/workspace/home.ts
  • src/lib/workspace/integration.ts
  • src/lib/workspace/lockfile.ts
  • src/lib/workspace/paths.ts
  • src/lib/workspace/project.ts
💤 Files with no reviewable changes (1)
  • src/lib/shared/path.ts

Comment thread src/lib/references/install.ts
Comment thread src/lib/references/remove.ts Outdated
Comment thread src/lib/sources/repository/__tests__/tags.test.ts
Comment thread src/lib/sources/repository/normalize.ts
Comment thread src/lib/workspace/paths.ts Outdated
@adelrodriguez

Copy link
Copy Markdown
Collaborator Author

@pullfrog review

@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from 06af555 to 6d20d8e Compare August 11, 2026 18:57

@pullfrog pullfrog 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.

Important

Direct repository entries introduce a two-segment package name (owner/repo), but the global store traversal still assumes only @scope/name can be nested. packref prune will read those entries back as bogus identities and delete them while projects still reference them.

Reviewed changes — full read of the 28-file diff for PR #38, which adds direct repository package specs alongside registry specs.

  • Discriminated ParsedPackageSpecRegistryPackageSpec (_tag: "registry") and RepositoryPackageSpec (_tag: "repository"), with registry resolution, remove, and the npm resolver narrowed to the registry variant.
  • Repository spec parsingcheckIsRepositorySpec / splitRepositoryRef / parseRepositoryIdentity in src/lib/core/packages.ts handle GitHub shorthand, provider shorthand, standard Git URLs, and SCP-style SSH URLs, falling back to the registry path when no supported provider is derived.
  • Ref resolution and pinningresolveDirectRepositoryRef resolves the requested tag, branch, commit, or default branch via a new listRefs, and git ls-remote now fetches all refs instead of only tags.
  • Reinstall pathfetchLockedStoreEntry skips tag matching for direct repository registries and reuses the pinned entry.version as the fetch ref.
  • Path helper consolidationgetDirectoryPath, getProjectLockfilePath, getGlobalDirectoryPath, and getGlobalConfigPath are inlined at their call sites; checkIsPathWithin moves from #lib/shared/path.ts into #lib/workspace/paths.ts.
  • Docs — README, agent guidance, and the add command/argument descriptions document the new spec forms.

I traced the classification order for the registry specs that already had coverage (react@19.0.0, @effect/cli@0.29.0, npm:react, npm: react, npm:@effect/cli@0.29.0, jsr:effect, pypi:requests): each either fails checkIsRepositorySpec or derives an undefined provider and falls through to the registry branch, so no existing spec is misclassified. The suite passes locally (289 tests).

⚠️ Two subdirectories of the same repository at the same ref cannot coexist

resolveDirectRepositoryRef builds the identity from name/registry/version only — repository.directory is not part of it. So packref add github:owner/repo/packages/a@v1 followed by packref add github:owner/repo/packages/b@v1 produces the same identity twice: upsertPackageEntry overwrites the first lockfile entry, and materializeStoreEntry reports the existing store entry as reused and hands back the first entry's source, so the second reference is silently reflinked to packages/a. The user gets the wrong directory with no error.

Technical details
# Directory is not part of the package identity for direct repository specs

## Affected sites
- `src/lib/sources/repository/normalize.ts:193-196` — identity is `{ name, registry, version }`; the resolved `directory` lives only on `repository.source`
- `src/lib/store/index.ts:196-215``materializeStoreEntry` keys on identity alone and returns the previously stored `source` when the entry exists, so a second add with a different directory reuses the first snapshot's source
- `src/lib/references/add.ts:180-193``createProjectReference` and the lockfile entry both consume `storeEntry.source`, propagating the wrong directory
- `src/lib/workspace/lockfile.ts``upsertPackageEntry` matches on `packageIdentityEquivalence`, so the second entry replaces the first

## Required outcome
- Two direct repository references that differ only by `directory` must either coexist correctly or be rejected with a clear error. Silently resolving the second to the first directory's contents is the outcome to eliminate.

## Open questions for the human
- Is monorepo-subdirectory support intended to be first-class here? If so, `directory` likely needs to participate in the identity (and therefore in the store path and lockfile key); if not, an explicit unsupported-combination error is the cheaper answer for this PR.

ℹ️ Nitpicks

  • The Optionundefined rewrite in src/commands/remove.ts and the inlining of the four #lib/workspace/paths.ts helpers are unrelated to direct repository specs; bundling them makes the feature diff harder to review and to revert independently.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/core/packages.ts
Comment thread src/lib/sources/repository/tags.ts Outdated
Comment thread src/lib/sources/repository/normalize.ts

@pullfrog pullfrog 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.

Important

The new commit fixes the ls-remote ref explosion and drops the unrelated refactors, but pinning tags to a SHA breaks packref remove owner/repo@v1.0. Two findings from the previous review are also still open.

Reviewed changes — delta between 06af555 (previous pullfrog review) and 6d20d8e, an amended force-push.

  • ls-remote scoped to refspecs — now git ls-remote <url> HEAD 'refs/heads/*' 'refs/tags/*', which restores the bound the --tags removal had lifted.
  • Unrelated refactors revertedsrc/commands/remove.ts, src/lib/workspace/paths.ts, src/lib/workspace/home.ts, and the src/lib/shared/path.ts deletion are back to their main state, leaving the diff focused on the feature.
  • Unsupported hosts now fail loudlyparseRepositoryIdentity returns the derived host, and an unambiguous repository locator (URL scheme or SCP form) on an unsupported host raises UnsupportedRepositoryHostError instead of falling through to a confusing "unsupported registry" error. The error message lists the supported providers.
  • Bare-host shorthandgithub.com/owner/repo, gitlab.com/owner/repo, bitbucket.org/owner/repo, and git.sr.ht/~owner/repo now parse, via a HOST_REPOSITORY_PROVIDERS lookup on the first path segment. The scpMatch === null comparisons were also corrected (the old !== undefined checks were always true).
  • Tag refs pin the commit — the tag match arm no longer special-cases slash-containing tags; every tag now resolves to a 12-character SHA.
  • New coveragesrc/lib/references/__tests__/add.test.ts asserts the full lockfile entry, store path, and materialized content for github:owner/repo; packages.test.ts adds the bare-host and unsupported-host cases.

Suite passes locally (297 tests, up from 289). I re-traced the registry-vs-repository classification against the new checkIsRepositorySpec and isUnambiguousRepositoryLocator guards — npm:react, npm: react, npm:@effect/cli@0.29.0, react@, jsr:effect, and pypi:requests all still reach the registry parser.

⚠️ Still open from the previous review

Neither of these is touched by the amended commit, so they carry forward rather than being restated here:

  • Global store traversal does not recognize two-segment repository nameslistStoreEntries still special-cases only @-prefixed segments, so packref prune reads owner/repo/<version> back as { name: "owner", version: "repo" } and deletes it as unreferenced. The new add.test.ts confirms the packages/github/owner/repo/<version> layout it mis-parses.
  • Two subdirectories of the same repository at the same ref collidedirectory is still absent from the identity, so the second add silently reuses the first snapshot's source.

The abbreviated-SHA round-trip question also still stands, and now applies to tag-resolved entries too: ref is the full 40-character SHA at add time while version — replayed as the fetch ref by install — is the 12-character prefix.

ℹ️ Nitpicks

  • Bare SourceHut shorthand ~user/repo (no sourcehut: prefix, no host) falls into the default-GitHub branch, and normalizeFromShorthandUrl strips the tilde, so it silently resolves to https://github.com/user/repo — a valid but wrong repository rather than an error. The newly supported git.sr.ht/~user/repo form covers the same intent, so treating a leading ~ as a SourceHut signal (or rejecting it) would close the gap.
  • The PR description still states "Tags and lightweight refs resolve to the ref name itself", which the new tag arm no longer does.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/sources/repository/normalize.ts
@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from 6d20d8e to 1eab264 Compare August 11, 2026 19:07

@pullfrog pullfrog 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.

Important

Both prior findings are genuinely fixed and the new tests pin exact values. Two things still need attention before merge: the CI format job fails on this commit, and the host-prefixed-shorthand fix covers only the four supported hosts — any other host-shaped first segment is still silently absorbed as the repository owner.

Reviewed changes — the delta 06af555..6d20d8e (13 files), read against the full PR diff, with parsePackageSpec executed directly on ~35 inputs and the repo's own check / format --check / typecheck / test commands run locally.

  • Fixed host-prefixed shorthand — a new HOST_REPOSITORY_PROVIDERS map lets a schemeless locator whose first segment is github.com / gitlab.com / bitbucket.org / git.sr.ht be split into host + path, so github.com/owner/repo now resolves to owner/repo instead of github.com/owner. Verified for all four hosts, plus git.sr.ht/~owner/repo and case-insensitive GitHub.com/....
  • Added an unsupported-host gatescheme:// and user@host:path locators on unrecognized hosts now fail with UnsupportedRepositoryHostError, whose message lists the four supported providers, instead of falling through to UnsupportedRegistryError.
  • Changed tag pinning to always store a SHAresolveDirectRepositoryRef no longer pins slash-free tags to the tag name; every resolved tag, branch, and HEAD now pins the 12-hex commit abbreviation, with tags.test.ts updated to exact ref/version pairs.
  • Narrowed ls-remote — the command now passes HEAD refs/heads/* refs/tags/* so refs/pull/* and refs/merge-requests/* are no longer transferred. Peeled refs/tags/v1^{} lines still arrive, so annotated tags still resolve to the commit.
  • Covered the direct-repository add path — the new add.test.ts case drives addPackageReference with github:owner/repo and asserts the exact lockfile entry, tracking: "manual", and the packages/github/owner/repo/<sha12> reference path.
  • Deduplicated the provider tablesnormalize.ts now imports SUPPORTED_REPOSITORY_PROVIDERS and REPOSITORY_PROVIDER_HOSTS from core/packages.ts rather than redeclaring them.
  • Reverted part of the unrelated refactorsrc/lib/shared/path.ts is restored, remove.ts is back on Option/Array.match with a named ReferenceSelection, and PackrefHomeValue is re-extracted. The four deleted workspace/paths.ts helpers remain inlined.
  • Cleared prior nitsscpMatch is now null-checked so the ${owner}/${repository} fallback is reachable, checkIsRepositorySpec no longer computes a slice(0, -1) prefix when there is no colon, and owner/repo@ now parses as a repository spec.

Two things I re-verified so nobody redoes them: no npm spec regressed (react@, @effect/cli@0.29.0, npm: react, npm:@effect/cli@0.29.0, lodash.get@4.4.2 all still parse as registry specs; jsr: / pypi: still raise UnsupportedRegistryError), and bun test --parallel is 297/297 with tsc --noEmit and bun run check clean.

ℹ️ Nitpicks

  • src/lib/sources/repository/normalize.ts:164-168 — pinning tags to a SHA means the requested tag is no longer recoverable from the entry: packref add owner/repo@v1.0.0 writes version: "333333333333" and nothing records v1.0.0, so packref list shows an opaque SHA and the PR description's "Tags and lightweight refs resolve to the ref name itself" is now stale. Also, an explicitly SHA-shaped specifier is still stored verbatim at whatever width the user typed (:179), so @<40 hex> and the 12-hex form of the same commit occupy two different store directories.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/sources/repository/tags.ts Outdated
Comment thread src/lib/core/packages.ts Outdated
Comment thread src/lib/core/packages.ts Outdated
Comment thread src/lib/core/packages.ts Outdated
@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from 1eab264 to 870b637 Compare August 11, 2026 20:48

@pullfrog pullfrog 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.

Important

Every prior finding is genuinely fixed and the new rejection gates are well tested, but three things landed in this commit that should be addressed before merge: the CI format job still fails (new file), bare SourceHut shorthand with a directory builds the wrong repository URL, and the new requestedRef field is stored in the shared store metadata, so it leaks across refs.

Reviewed changes — the delta 6d20d8e..870b637 (15 files), read against the full 30-file PR diff, with parsePackageSpec + normalizeRepositorySource executed directly on the new locator forms and the repo's own test / typecheck / format commands run locally.

  • Rewrote parseRepositoryIdentity — now an Effect.fn dispatching to five pure builders (identityFromShorthand, identityFromStandardUrl, identityFromScpUrl, identityFromKnownHostPath, identityFromBarePath) over shared splitLocatorPath / formatRepositoryName helpers, replacing the single interleaved function.
  • Added three rejection gates — unsupported host-shaped shorthand (git.mycompany.com/owner/repo, bitbucket.org.evil.com/owner/repo) now raises UnsupportedRepositoryHostError; a locator with no repository name (https://github.com/owner, github:owner, sourcehut:~owner) raises InvalidPackageIdentity; host-qualified GitLab locators with a third path segment are rejected as nested subgroups. All three have exact-value tests.
  • Added bare SourceHut shorthand~owner/repo (no prefix, no host) now routes to sourcehut instead of silently resolving to https://github.com/owner/repo.
  • Changed pinning to the full 40-hex SHAPINNED_SHA_LENGTH is gone, so HEAD, tag, and branch resolutions all store the complete SHA; tags.test.ts, add.test.ts, and install.test.ts were updated to the wider values.
  • Recorded the requested refRepositorySource gains an optional requestedRef, and findPackageEntries matches spec.specifier against entry.version or entry.source.requestedRef, so packref remove owner/repo@v1.0.0 finds a SHA-pinned entry.
  • Fixed store traversal for two-segment nameslistStoreEntries treats a segment as nested when it starts with @ or the registry is a repository provider, so prune no longer misreads owner/repo/<version>. Covered by a new store.test.ts case.
  • Rejected a second directory for the same identityaddDirectRepositoryReferenceToProject raises StoreSourceMismatchError when the reused store entry's directory differs, and both add tails were extracted into addMaterializedReferenceToProject.
  • Tightened the install path — a direct repository entry whose source has no fetchSource now raises UnsupportedRepositoryHostError instead of SnapshotFetchError.

I re-ran the classification check so nobody redoes it: react, react@, react@19.0.0, @effect/cli@0.29.0, npm:react, npm: react, npm:@effect/cli@0.29.0, lodash.get@4.4.2 all still parse as registry specs, and jsr: / pypi: still raise UnsupportedRegistryError. bun test is 311/311 (up from 297) and bunx tsc --noEmit is clean.

ℹ️ Nitpicks

  • README.md:83-91 lists four spec forms, but this commit added two more that only appear in the PR description: host-prefixed shorthand (github.com/owner/repo, git.sr.ht/~owner/repo) and bare SourceHut shorthand (~owner/repository). Worth adding so the newly supported inputs are discoverable.
  • An explicitly SHA-shaped specifier is still stored verbatim at whatever width the user typed (normalize.ts:171-174), while every resolved path now stores 40 hex, so @abcdef1 and the full SHA of that same commit occupy two different store directories.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/core/packages.ts Outdated
Comment thread src/lib/core/packages.ts Outdated
Comment thread src/lib/sources/repository/fetch.ts Outdated
Comment thread src/lib/core/packages.ts Outdated
@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from 870b637 to d08356b Compare August 12, 2026 14:48
@adelrodriguez

Copy link
Copy Markdown
Collaborator Author

@pullfrog review

@pullfrog

pullfrog Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@adelrodriguez

Copy link
Copy Markdown
Collaborator Author

@pullfrog review

@pullfrog pullfrog 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.

Important

Two subdirectories of the same repository at the same commit share one PackageIdentity, so adding the second one fails with a StoreSourceMismatchError that poisons the entry machine-wide — and the error text points the user at a remediation that reproduces it.

Reviewed changes

This PR teaches packref to accept repository locators directly (github:owner/repo@ref, owner/repo, full clone URLs, SCP-like URLs) alongside registry specs, resolving refs through git ls-remote and snapshotting via giget.

  • Spec parsing became a discriminated unionparsePackageSpec now returns RegistryPackageSpec | RepositoryPackageSpec, with repository locators detected ahead of the registry path via provider shorthands, owner/repo shape, and URL shape.
  • Four providers are recognizedSUPPORTED_REPOSITORY_PROVIDERS covers bitbucket, github, gitlab, and sourcehut, mapped to hosts and kept deliberately separate from SUPPORTED_REGISTRIES.
  • Ref resolution widened beyond tagsgit ls-remote now requests HEAD refs/heads/* refs/tags/* and the new parseGitRemoteRefsOutput returns {head, heads, tags}, so bare locators pin HEAD and requested refs match tags, branches, or a literal 40-hex SHA.
  • requestedRef is recorded lockfile-onlyfetchRepositorySnapshot strips it from store metadata, so packref remove owner/repo@v1.0.0 still resolves against a SHA-pinned entry while the store stays content-addressed.
  • Store traversal understands provider registrieslistStoreEntries treats provider-named registries like scoped names so owner/repo round-trips through packref list.
  • src/lib/workspace/paths.ts shrank to constants — its four path helpers were deleted and inlined at their call sites.

⚠️ Two subdirectories of the same repository at one commit cannot coexist

PackageIdentity is {name, registry, version} and has no directory component, so github:owner/repo/packages/a and github:owner/repo/packages/b at the same commit resolve to the identical identity and therefore the identical store path. The store entry is written once (materializeStoreEntry is first-writer-wins), so its metadata permanently records whichever directory was added first; the new guard in src/lib/references/add.ts then rejects the second subdirectory with StoreSourceMismatchError. Because the global store is shared, the first add poisons that repo+commit for every project on the machine.

Technical details
# Subdirectory is part of the source but not part of the identity

## Affected sites

- `src/lib/core/packages.ts` — `getPackageIdentitySegments` keys store paths on `{name, registry, version}` only; `parseRepositoryIdentity` folds the locator's subdirectory into `source.directory`, not into `name`.
- `src/lib/sources/repository/fetch.ts` — the stored source strips `requestedRef` but **keeps** `directory`, so `directory` is durable store metadata.
- `src/lib/store/index.ts` — `materializeStoreEntry` returns `{reused: true, source: storedEntry.source}` when the path exists and never rewrites metadata.
- `src/lib/references/add.ts` — the new guard fails when `storeEntry.source.directory !== resolved.repository.source.directory`.
- `src/lib/workspace/project.ts` — `createProjectReference` resolves `directory` against `storePath` at reflink time, confirming the store holds the whole repository and `directory` is only a per-reference subpath selector.
- `docs/plans/11-direct-repository-sources.md` (lines 64-66) — states subdirectory locators are supposed to work.

## Required outcome

Two references to different subdirectories of the same repository at the same commit must both succeed. Since the snapshot on disk is the full repository and `directory` is applied only when reflinking into `.packref/`, the mismatch is not a real conflict.

## Suggested approach

Either drop `directory` from the store metadata entirely (it is a reference-time concern, and `packageSnapshotSourceEquivalence` in `src/lib/references/install.ts` would need the same treatment), or include the subdirectory in the identity so the two references get distinct store paths.

## Open questions for the human

`src/lib/core/errors.ts` (~line 262) tells the user the entry "does not match the source recorded in packref-lock.json" and to run `packref clean --global` and retry. On the add path nothing is in the lockfile yet, and after a clean the same first-writer-wins sequence reproduces the failure. Should this message be rewritten regardless of how the identity question is resolved?

⚠️ No test exercises the giget fetch contract this PR now depends on

Every new test stubs git ls-remote output and the RepositoryDownloader, so the suite proves the parsing and routing logic but never that giget can actually fetch the refs this PR chooses to pin. The plan's own step 7 (a real-network integration test) and its note to verify that giget/codeload accepts full commit SHAs are both still outstanding.

Technical details
# The provider archive contract is unverified

## Affected sites

- `src/lib/sources/repository/normalize.ts` — `COMMIT_SHA_PATTERN` accepts any 40-hex string as a pinnable ref for all four providers.
- `src/lib/sources/repository/fetch.ts` — passes `${fetchSource}#${ref}` straight to giget's `downloadTemplate`.
- `docs/plans/11-direct-repository-sources.md` — step 7 requires a real-network integration test; the plan also flags verifying SHA acceptance.

## Required outcome

At least one test (or a documented manual verification) that a full commit SHA resolves to a real archive for each supported provider, so a SHA pin does not fail only in the field.

## Open questions for the human

giget builds sourcehut archives as `https://git.sr.ht/~${repo}/archive/${ref}.tar.gz`. GitHub and Bitbucket document arbitrary-commit archives and GitLab very likely supports them; sourcehut's behaviour for a non-tip SHA is the one I could not confirm. Is sourcehut SHA pinning something you have exercised by hand, or should it be restricted to tags and branches until it is?

ℹ️ The plan doc no longer describes the implemented pinning rules

docs/plans/11-direct-repository-sources.md ships in this PR still marked "Not started" (line 11), and its pinning section describes behaviour the code does not implement: lines 55-56 say bare locators pin the abbreviated 12-hex SHA (the code pins the full 40-hex SHA) and lines 57-58 say tags are pinned verbatim (the code resolves the tag to its SHA).

Technical details
# Plan and implementation disagree on what gets pinned

## Affected sites

- `docs/plans/11-direct-repository-sources.md` line 11 — status still "Not started".
- `docs/plans/11-direct-repository-sources.md` lines 55-58 — abbreviated-SHA and verbatim-tag pinning.
- `src/lib/sources/repository/normalize.ts` — `resolveDirectRepositoryRef` sets `version` to the full resolved SHA in every branch.

## Required outcome

The plan either reflects the shipped pinning rules and its real status, or records explicitly that the rules changed and why (full SHAs are unambiguous and immutable, which reads like the better choice).

ℹ️ Nitpicks

  • src/lib/references/__tests__/add.test.ts (~249-251) and src/lib/sources/repository/__tests__/tags.test.ts (~330-334): the new .rejects assertions are neither awaited nor returned, so a broken expectation would surface as an unhandled rejection rather than a failing test. This matches existing house style at ~8 other sites, so it is only worth fixing if you want to start unwinding the pattern.
  • src/lib/core/__tests__/packages.test.ts (~338-348): it("builds direct repository identity segments") sits inside describe("parsePackageSpec") but tests identity segmentation, and its fixture version "abc123def456" is a 12-hex string that the implementation would never produce now that refs pin to full SHAs.
  • src/lib/workspace/paths.ts: deleting the four path helpers is unrelated to direct repository sources and spreads path.join(projectPath, PACKREF_DIRECTORY_NAME) across 6+ files. Not wrong, just extra diff to review alongside a feature.
  • src/lib/sources/repository/normalize.ts: the Match.value chain in resolveDirectRepositoryRef needs six ?? "" fallbacks because Match.when predicates do not narrow the matched value. A plain if/else ladder over the same four cases would narrow naturally and drop all six.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/core/packages.ts Outdated

@pullfrog pullfrog 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.

Important

Adding two subdirectories of the same repository at the same commit dead-ends with a StoreSourceMismatchError whose prescribed remedy cannot resolve it. That is the primary monorepo use case for the [/directory] form this PR documents.

Reviewed changes — full initial review of the direct repository package spec feature across all 30 files.

  • ParsedPackageSpec becomes a discriminated unionRegistryPackageSpec (_tag: "registry") and RepositoryPackageSpec (_tag: "repository"); registry resolution, lockfile lookup, and add/remove all branch on the tag.
  • Repository locator parsingcheckIsRepositorySpec plus five identity constructors in src/lib/core/packages.ts cover provider shorthand, host-prefixed shorthand, bare owner/repo, SourceHut ~owner, standard/git+ URLs, and SCP-style SSH, each with optional subdirectory and @ref.
  • Ref resolution and pinningresolveDirectRepositoryRef reads HEAD, refs/heads/*, and refs/tags/* via a new RemoteTagReader.listRefs, resolving tags (including peeled annotated tags), branches, and full 40-hex SHAs to a commit SHA that becomes the pinned version.
  • Requested ref trackingRepositorySource gains an optional requestedRef, written only into the project lockfile (fetchRepositorySnapshot strips it from store metadata), so packref remove owner/repo@v1.0.0 still resolves a SHA-pinned entry.
  • Install path — direct repository entries skip tag re-resolution and fetch entry.version verbatim; packageSnapshotSourceEquivalence compares repository sources field-by-field so the lockfile-only requestedRef does not trip the store mismatch check.
  • Store layoutgetPackageIdentitySegments and listStoreEntries both handle two-segment owner/repo names, keeping repository entries visible to list and prune.
  • Unrelated refactorsrc/lib/workspace/paths.ts loses its four path helpers, inlined as path.join(...) across ten call sites.

I ran bunx tsc --noEmit (clean) and bun test (314 pass, 0 fail) on the head commit. I also verified independently that all four providers' archive endpoints giget builds accept a full 40-hex commit SHA, so the change from tag-refs to SHA-refs on reinstall is safe, and that every spec form the README advertises routes to the repository branch correctly.

ℹ️ Nitpicks

  • The src/lib/workspace/paths.ts helper removal and its ten inlined path.join(...) call sites are unrelated to direct repository sources. Bundling them here makes the feature harder to revert on its own.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/references/add.ts
Comment thread src/lib/core/packages.ts
Comment thread src/lib/workspace/integration.ts Outdated
Comment thread src/lib/core/packages.ts Outdated
@adelrodriguez
adelrodriguez force-pushed the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch from d08356b to 3739330 Compare August 12, 2026 15:10
@adelrodriguez

Copy link
Copy Markdown
Collaborator Author

@pullfrog review

@pullfrog

pullfrog Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Your Claude subscription has hit its usage limit. It resets at 5:40pm (UTC). Re-trigger Pullfrog after the reset, or add an ANTHROPIC_API_KEY repo secret — Pullfrog routes around an exhausted subscription automatically when one is present.

Add repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@pullfrog

pullfrog Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Your Claude subscription has hit its usage limit. It resets at 5:40pm (UTC). Re-trigger Pullfrog after the reset, or add an ANTHROPIC_API_KEY repo secret — Pullfrog routes around an exhausted subscription automatically when one is present.

Add repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@pullfrog pullfrog 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 new issues found.

Reviewed changes since the prior Pullfrog review at d08356b:

  • Separated snapshot and project directory metadata — Removed the selected repository directory from shared store metadata while retaining it in each project lockfile, so different projects can reuse one full-repository source snapshot with different directory selections.
  • Added a clear same-project conflict — Added RepositoryDirectoryConflictError before snapshot fetching when one project requests a second directory for the same package identity, with a remedy that resolves the conflict.
  • Preserved reinstall equivalence — Updated direct-repository snapshot fetching and source comparison to ignore project-only directory and requestedRef fields while still validating repository host and URL.
  • Closed parser and guidance gaps — Kept npm aliases on the registry parser path, removed the unused hostQualified field, and documented the full 40-character commit SHA requirement in generated agent guidance.
  • Restored shared path helpers — Reintroduced the project and global path helpers and replaced the unrelated inlined path construction.
  • Added regression coverage — Added exact tests for same-project conflicts, cross-project directory reuse, npm aliases, and per-project requested refs.

Verification passed: bun run format --check, bun run check, bunx tsc --noEmit, and 100 affected tests.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@adelrodriguez
adelrodriguez merged commit 564d46c into main Aug 12, 2026
8 checks passed
@adelrodriguez
adelrodriguez deleted the 08-11-add_direct_repository_package_specs_for_github_gitlab_bitbucket_and_sourcehut branch August 12, 2026 16:02
adelrodriguez added a commit that referenced this pull request Aug 12, 2026
…lpers

Plan 11 shipped in #38 but its status still said "Not started"; mark it
Complete to match plan 10's convention and point its two code
references at the helper names introduced by this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
adelrodriguez added a commit that referenced this pull request Aug 12, 2026
…pers (#42)

* Consolidate package reference orchestration and shared formatting helpers

Remove the addPackageCandidateReference wrapper so the two-phase
resolve/materialize API is the single path for candidate adds, delegate
the manifest-dependency branch of addPackageReference to it so the
manifest range computation has one home, and rename the near-identical
add.ts helpers to distinct names (recordStoreEntryReference,
fetchAndRecordResolvedPackage, addRepositoryReference).

Also extract formatPackageIdentity into core/packages.ts to replace
three copied formatIdentity definitions plus two inlined templates, and
encode the prune store-safety condition once as canPruneStore, shared
by discoverPrunePlan and applyPrunePlan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update plan 11 to reflect its completed implementation and renamed helpers

Plan 11 shipped in #38 but its status still said "Not started"; mark it
Complete to match plan 10's convention and point its two code
references at the helper names introduced by this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <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