Quickstart
Quickstart
Run from a project that wants to vendor upstream packages:
Or install the CLI globally with Homebrew if you'd rather have inrepo on your $PATH:
brew install inrepo and npx inrepo install the same npm-published artifact, so the rest of this guide works either way. Substitute inrepo for npx inrepo when you have it installed locally.
Initialize config explicitly, or let the first sync or add prompt for where config should live:
Add and pin a package:
If the npm registry package does not expose a GitHub repository URL, pass the git URL:
Use -D when the generated local package should be wired into devDependencies:
Add --with-deps when you want the package's runtime dependencies vendored as source too:
The normal collaboration loop is:
- Run
npx inrepo syncto rebuild generated modules frominrepo.lock.jsonandinrepo_patches/. - Edit files under
inrepo_modules/<package>/. - Run
npx inrepo patch <package> -m "reason"to capture those edits intoinrepo_patches/<package>/. - Run
npx inrepo diff <package>to review the effective change. - Commit config, lockfile changes, and patch files.
- Teammates pull and run
npx inrepo sync.
Commit these files:
inrepo.jsonorpackage.json#inrepoinrepo.lock.jsoninrepo_patches/
Keep these generated files out of git:
inrepo_modules/.inrepo/
Before merging, run:
verify checks that generated module trees still match the lockfile plus committed changes.
Vendoring transitive dependencies
For registry packages, --with-deps reads the exact published dependencies, including npm's rewritten workspace ranges. A manual --git root uses its checkout manifest. It resolves each range to an exact published version and immutable repository commit, preferring npm's publish-time gitHead, then a matching release tag, then registry-hosted npm provenance cross-checked against the tarball digest and repository. Only runtime dependencies are followed: devDependencies, peerDependencies, and optionalDependencies are not added automatically.
The resolved tree is printed before anything is written:
The root keeps its package name, while each graph-managed dependency receives a versioned module identity. citty@0.1.6 and citty@0.2.2, for example, can coexist at inrepo_modules/citty@0.1.6 and inrepo_modules/citty@0.2.2. Graph edges retain the bare import name but point to the exact module instance selected for that dependent. Compatible instances are reused, and re-running --with-deps completes the missing part of an existing graph.
For each registry dependency, the selected npm tarball URL and integrity are retained too. inrepo verifies and caches that exact payload, then fills only files missing from the git checkout, restoring publish-only runtime output without overwriting repository source or package.json. These generated base files stay out of diffs and captured patches. A manual --git root does not use registry artifacts.
For a scoped dependency, @scope/pkg@1.2.3 is materialized at inrepo_modules/@scope/pkg@1.2.3. Its generated config entry retains name: "@scope/pkg" for imports and uses module: "@scope/pkg@1.2.3" for config, lock, generated tree, and patch identity.
npm repository.directory metadata is preserved automatically. If it is absent, registry dependency resolution scans the immutable checkout for a unique package manifest matching the published name and version, then records that directory. The package subtree becomes the module root, so keep/exclude filters and patch paths remain package-relative. Packages sharing one repository commit reuse the same raw snapshot, while their module trees stay separate. Strict owner/repo npm repository shorthand is recognized as GitHub; manual monorepo sources can use --repository-directory <path>.
The edges are recorded under graph in inrepo.lock.json. A versioned module instance uses version 4; recording published artifacts raises the file to lockfileVersion: 5. Versions 1–4 remain readable. Every dependency entry pins an exact git URL and immutable commit, plus artifact integrity when needed, so sync and verify replay and check the whole graph offline after caches are populated.
Resolution fails before anything is vendored when no published version satisfies an individual range, when a dependency uses a source that cannot be pinned (workspace:, file:, link:, catalog:, npm: aliases, git or tarball URLs, dist-tags), when source-directory discovery is missing or ambiguous, or when registry metadata cannot establish both a usable repository and an immutable source commit. Each message names the dependency and reason. Private monorepo packages can be added with --git <url> --repository-directory <path> --ref <ref>. Compatible ranges reuse an instance; incompatible ranges resolve to separate versioned instances.
Bare specifiers stay as upstream wrote them unless rewireImports is enabled. --with-deps also cannot be combined with --no-save, because a graph is only replayable from committed files.
Patch series
A package's committed changes live in inrepo_patches/<package>/ in one of two formats:
series/0001-*.patch— an ordered git patch series. Each file is standardgit format-patch --binaryoutput, andsyncapplies them in filename order withgit am --3wayon top of the pinned upstream commit. Reviews show effective hunks instead of whole replacement files.- Whole-file snapshots plus
.inrepo-deletions— the original overlay format, still used by any package that already has snapshot files.
New captures go into the series:
patch compares inrepo_modules/<package> against the patched tree — the pinned upstream commit plus the patches already committed — and appends the difference as the next numbered patch. Each run writes a new patch, so -m is required and becomes its subject. If nothing changed, the command says so and writes nothing.
The patch headers are the provenance record: From:, Date:, and Subject: carry who changed what and why, so there is no separate manifest to keep in sync. The author is taken from your git user.name and user.email.
Reviewing the delta
diff prints the effective change from the pinned upstream commit to the patched tree, preceded by the patch series that produced it and each patch's subject, author, and date. --stat replaces the hunks with a per-file +/- summary. Git renders the diff, so deletions, mode changes, symlinks, and binary files all read correctly, and snapshot-format packages are covered too. diff is a viewer: it exits 0 whether or not there are differences.
Updating to a newer upstream commit
update re-resolves the pinned ref, then rebases the committed patch series onto the new upstream commit in a scratch git repository. A clean rebase rewrites the series — renumbered from 0001, with each patch's original subject, author, and date preserved — updates inrepo.lock.json, saves a --ref back to your config, and rebuilds inrepo_modules/<package>. Patches upstream has since adopted are dropped. None of that happens until the rebase finishes, so an update that fails changes nothing.
Packages with no patches are re-pinned and rebuilt. Packages still on the snapshot format have to be migrated first.
When the package is part of a recorded dependency graph, update also moves its version in graph and the resolved version on every edge pointing at it, so verify stays clean. Ranges are left alone — only add --with-deps re-resolves those — so a new version that no longer satisfies a dependent's recorded range is reported as a warning naming the dependent and the range.
Resolving conflicts
When a patch and upstream changed the same lines, update stops, names the patch it stopped on, and lists the conflicted files. The half-finished rebase stays in .inrepo/updates/<package>/repo, a normal git work tree with normal conflict markers. Edit the files there — staging is handled for you — and then:
--continue resumes the rebase and stops again if a later patch also conflicts; a resolution that leaves a patch with no effect drops that patch. --abort deletes the scratch repository and leaves the project as it was. While an update is in progress, starting another one for the same package is refused.
Migrating a snapshot overlay
Convert a package from the snapshot format to a series with:
migrate writes series/0001-<subject>.patch, then deletes the snapshot files only after confirming that replaying the series over the pinned upstream commit reproduces exactly the same tree, including binary files, deletions, symlinks, and executable bits. If anything differs, the snapshot overlay is left untouched and the command explains what did not match. Empty directories cannot be part of a patch series because git does not record them; migrate reports any it drops.
Safety checks
inrepo tries not to silently destroy local work.
If inrepo_modules/ changed but inrepo_patches/ did not, sync treats that as uncaptured work and asks you to run npx inrepo patch. If both changed, sync reports a conflict.
Use npx inrepo sync --force only when you want to discard generated edits. Before doing that, inrepo saves a backup under .inrepo/backups/.