git.delta.rocks / jrsonnet / refs/commits / c93de7763652

difftreelog

feat jrb

orkooprxYaroslav Bolyukin2026-05-06parent: #3732ed2.patch.diff
in: master

11 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
 version = 4
 
 [[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
 name = "ahash"
 version = "0.8.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -109,7 +115,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -120,7 +126,7 @@
 dependencies = [
  "anstyle",
  "once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -139,18 +145,61 @@
 ]
 
 [[package]]
+name = "arc-swap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
 name = "arraydeque"
 version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
 
 [[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
 name = "autocfg"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
+name = "aws-lc-rs"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
+[[package]]
 name = "base64"
 version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -164,6 +213,15 @@
 
 [[package]]
 name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
 version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
@@ -178,6 +236,7 @@
 checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
 dependencies = [
  "memchr",
+ "regex-automata",
  "serde",
 ]
 
@@ -191,6 +250,33 @@
 ]
 
 [[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bytesize"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
+
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
 name = "cast"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -203,6 +289,8 @@
 checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
 dependencies = [
  "find-msvc-tools",
+ "jobserver",
+ "libc",
  "shlex",
 ]
 
@@ -295,12 +383,40 @@
 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
 
 [[package]]
+name = "clru"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5"
+dependencies = [
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
 name = "colorchoice"
 version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
 
 [[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
 name = "console"
 version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -308,7 +424,7 @@
 dependencies = [
  "encode_unicode",
  "libc",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -328,6 +444,22 @@
 checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
 
 [[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
 name = "countme"
 version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -335,6 +467,15 @@
 
 [[package]]
 name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
@@ -343,6 +484,15 @@
 ]
 
 [[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
 name = "criterion"
 version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -378,6 +528,15 @@
 ]
 
 [[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
 name = "crossbeam-deque"
 version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -410,6 +569,16 @@
 
 [[package]]
 name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
@@ -418,17 +587,62 @@
 ]
 
 [[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+]
+
+[[package]]
 name = "digest"
 version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.12.0",
  "const-oid",
- "crypto-common",
+ "crypto-common 0.2.1",
 ]
 
 [[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "displaydoc"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -461,6 +675,12 @@
 checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
 
 [[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
 name = "educe"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -535,7 +755,7 @@
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -550,18 +770,49 @@
 ]
 
 [[package]]
+name = "faster-hex"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
+dependencies = [
+ "heapless",
+ "serde",
+]
+
+[[package]]
 name = "fastrand"
 version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
 
 [[package]]
+name = "filetime"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+]
+
+[[package]]
 name = "find-msvc-tools"
 version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
 
 [[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
 name = "fnv"
 version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -589,7 +840,85 @@
 ]
 
 [[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
 name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
 version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
@@ -616,6 +945,940 @@
 ]
 
 [[package]]
+name = "gix"
+version = "0.83.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce52001b946a6249d5d0d3011df0a042ac3f8a4d013460db6476577b0b9c567"
+dependencies = [
+ "gix-actor",
+ "gix-archive",
+ "gix-attributes",
+ "gix-blame",
+ "gix-command",
+ "gix-commitgraph",
+ "gix-config",
+ "gix-credentials",
+ "gix-date",
+ "gix-diff",
+ "gix-dir",
+ "gix-discover",
+ "gix-error",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-ignore",
+ "gix-index",
+ "gix-lock",
+ "gix-mailmap",
+ "gix-merge",
+ "gix-negotiate",
+ "gix-object",
+ "gix-odb",
+ "gix-pack",
+ "gix-path",
+ "gix-pathspec",
+ "gix-prompt",
+ "gix-protocol",
+ "gix-ref",
+ "gix-refspec",
+ "gix-revision",
+ "gix-revwalk",
+ "gix-sec",
+ "gix-shallow",
+ "gix-status",
+ "gix-submodule",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-transport",
+ "gix-traverse",
+ "gix-url",
+ "gix-utils",
+ "gix-validate",
+ "gix-worktree",
+ "gix-worktree-state",
+ "gix-worktree-stream",
+ "nonempty",
+ "parking_lot",
+ "regex",
+ "signal-hook",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-actor"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "272916673b83714734b15d4ef3c8b5f1ccddb15fea8ff548430b97c1ab7b7ed8"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-error",
+]
+
+[[package]]
+name = "gix-archive"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a20ec244b733338d4cb60e5e05eac700dab7fcc689647b1d1daa9396b119342"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-error",
+ "gix-object",
+ "gix-worktree-stream",
+]
+
+[[package]]
+name = "gix-attributes"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe17c5a1c0b6f2ef1476aa1d3222ea50cdff67608016613a58bfc3e078046000"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "kstring",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-bitmap"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ecbfc77ec6852294e341ecc305a490b59f2813e6ca42d79efda5099dcab1894"
+dependencies = [
+ "gix-error",
+]
+
+[[package]]
+name = "gix-blame"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dab9a942ab54a9661ded7397c3bf927274e7afa94494db0d75cfcbde02ca0a"
+dependencies = [
+ "gix-commitgraph",
+ "gix-date",
+ "gix-diff",
+ "gix-error",
+ "gix-hash",
+ "gix-object",
+ "gix-revwalk",
+ "gix-trace",
+ "gix-traverse",
+ "gix-worktree",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-chunk"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edf288be9b60fe7231de03771faa292be1493d84786f68727e33ad1f91764320"
+dependencies = [
+ "gix-error",
+]
+
+[[package]]
+name = "gix-command"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86335306511abe43d75c866d4b1f3d90932fe202edcd43e1314036333e7384d8"
+dependencies = [
+ "bstr",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "shell-words",
+]
+
+[[package]]
+name = "gix-commitgraph"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe3b5aa0f24e19028c261d229aeeedafcaaa52ebd71021cc15184620fc9d32eb"
+dependencies = [
+ "bstr",
+ "gix-chunk",
+ "gix-error",
+ "gix-hash",
+ "memmap2",
+ "nonempty",
+]
+
+[[package]]
+name = "gix-config"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c01848aebd21c67f6ba41f1de8efd46ae96df21f001954a3c9e1517e514d410"
+dependencies = [
+ "bstr",
+ "gix-config-value",
+ "gix-features",
+ "gix-glob",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-config-value"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b39ed39ee4c10a3b157f9fb94bac8098d9f8e56201f0cf7dee6c187416c4b2"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-path",
+ "libc",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-credentials"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65ca11598b70811d7b16ff90945a6e57dfe521e85b744e51636965fe39cc8f60"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-config-value",
+ "gix-date",
+ "gix-path",
+ "gix-prompt",
+ "gix-sec",
+ "gix-trace",
+ "gix-url",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-date"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543"
+dependencies = [
+ "bstr",
+ "gix-error",
+ "itoa",
+ "jiff",
+ "smallvec",
+]
+
+[[package]]
+name = "gix-diff"
+version = "0.63.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc08e0fa1a91ff5f24affeab052f198056645e1de004910bde7b82b50ea5982a"
+dependencies = [
+ "bstr",
+ "gix-attributes",
+ "gix-command",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-imara-diff",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-traverse",
+ "gix-worktree",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-dir"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a0fc06e9e1e430cbf0a313666976d90f822f461a6525320427aa9b8af5236c"
+dependencies = [
+ "bstr",
+ "gix-discover",
+ "gix-fs",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-trace",
+ "gix-utils",
+ "gix-worktree",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-discover"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17852e6a501e688a1702b24ebe5b3761d4719455bc869fd29f38b0b859bcad34"
+dependencies = [
+ "bstr",
+ "dunce",
+ "gix-fs",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-error"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613"
+dependencies = [
+ "bstr",
+]
+
+[[package]]
+name = "gix-features"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af375693ad5333d0a2c66b4c5b2cbe9ccc38e34f8e8bf24e4ae42c12307fdc4f"
+dependencies = [
+ "bytes",
+ "bytesize",
+ "crc32fast",
+ "crossbeam-channel",
+ "gix-path",
+ "gix-trace",
+ "gix-utils",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "prodash",
+ "thiserror",
+ "walkdir",
+ "zlib-rs",
+]
+
+[[package]]
+name = "gix-filter"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac917dbe9653c9b615d248db91907a365bd779750c9e1b457a9d9fdeece3a08"
+dependencies = [
+ "bstr",
+ "encoding_rs",
+ "gix-attributes",
+ "gix-command",
+ "gix-hash",
+ "gix-object",
+ "gix-packetline",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "gix-utils",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-fs"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e1967daac9848757c47c2aef0c57bcadc1a897347f559778249bf286a536c86"
+dependencies = [
+ "bstr",
+ "fastrand",
+ "gix-features",
+ "gix-path",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-glob"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bf29249a069bf2507f5964f80997f37b134d320ea348d66527726b9be2c38c"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-features",
+ "gix-path",
+]
+
+[[package]]
+name = "gix-hash"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf70d1e252337eed16360f8b8ebb71865ece58eab7954b39ce38b420de703d2"
+dependencies = [
+ "faster-hex",
+ "gix-features",
+ "sha1-checked",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-hashtable"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b"
+dependencies = [
+ "gix-hash",
+ "hashbrown 0.16.1",
+ "parking_lot",
+]
+
+[[package]]
+name = "gix-ignore"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb13fbbeeafee943e52b61fcc88dfddf6a452fcaf0c4d0cdc8f218fa25bbec5"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-trace",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-imara-diff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39eb0623e15e4cb83c02ce6a959e48fadd1ae3b715b36b5acc01816e01388c82"
+dependencies = [
+ "bstr",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "gix-index"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54c3ef97ad08121e4327a6226bd63fed6b9e3c6b976d48bddd4356d9d41191db"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "filetime",
+ "fnv",
+ "gix-bitmap",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-traverse",
+ "gix-utils",
+ "gix-validate",
+ "hashbrown 0.16.1",
+ "itoa",
+ "libc",
+ "memmap2",
+ "rustix",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-lock"
+version = "23.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3bc074e5723027b482dcd9ab99d95804a53742f6de812d0172fbba4a186c1"
+dependencies = [
+ "gix-tempfile",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-mailmap"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "023d3a6561cbebe45b89e0764d48928ad970667076f16fa5889e6f86d8432086"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "gix-error",
+]
+
+[[package]]
+name = "gix-merge"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74bbcdcc52b70a32f0a151b024dff9d0fcf56ee48f00d9503e735af9d99ea881"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-diff",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-imara-diff",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-quote",
+ "gix-revision",
+ "gix-revwalk",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-worktree",
+ "nonempty",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-negotiate"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "103d42bfade1b8a96ca5005933127bdad461ce588d92422b2c2daa3ff20d780c"
+dependencies = [
+ "bitflags",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-object",
+ "gix-revwalk",
+]
+
+[[package]]
+name = "gix-object"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a38075a95d7cc5df8afd38e72c617026c1456952207a4120a7f55a3fbf93b4d7"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-utils",
+ "gix-validate",
+ "itoa",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-odb"
+version = "0.80.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeeda12a9663120418735ecdc1250d06eeab0be75700e47b3402a981331716ba"
+dependencies = [
+ "arc-swap",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-pack",
+ "gix-path",
+ "gix-quote",
+ "memmap2",
+ "parking_lot",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-pack"
+version = "0.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf02e6f5c8f07a069c9ea5245f40d9b14856ada4086091dc99941b49002b4fa"
+dependencies = [
+ "clru",
+ "gix-chunk",
+ "gix-error",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "memmap2",
+ "parking_lot",
+ "smallvec",
+ "thiserror",
+ "uluru",
+]
+
+[[package]]
+name = "gix-packetline"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "362246df440ee691699f0664cbf7006a6ece477db6734222be95e4198e5656e6"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-path"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "671a6059e8a4c1b7f406e24716499cefa3926e060876fb1959ef225efeee346e"
+dependencies = [
+ "bstr",
+ "gix-trace",
+ "gix-validate",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-pathspec"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a84a4f083dd70fb49f4377e13afa6d90df2daaa1c705c49d6ff1331fc7e8855"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-attributes",
+ "gix-config-value",
+ "gix-glob",
+ "gix-path",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-prompt"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e041a626c64cb69e4117fcdf80da8d0e454fba3b1f420412792d191f52251aee"
+dependencies = [
+ "gix-command",
+ "gix-config-value",
+ "parking_lot",
+ "rustix",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-protocol"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4bee82db63ec635996b96efae71cf467c155fa3f34a556184373224a26c4fd"
+dependencies = [
+ "bstr",
+ "gix-credentials",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-lock",
+ "gix-negotiate",
+ "gix-object",
+ "gix-ref",
+ "gix-refspec",
+ "gix-revwalk",
+ "gix-shallow",
+ "gix-trace",
+ "gix-transport",
+ "gix-utils",
+ "maybe-async",
+ "nonempty",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-quote"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e97b73791a64bc0fa7dd2c5b3e551136115f97750b876ed1c952c7a7dbaf8be"
+dependencies = [
+ "bstr",
+ "gix-error",
+ "gix-utils",
+]
+
+[[package]]
+name = "gix-ref"
+version = "0.63.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8ba9cc15f558b274c99349b83130f5ec83459660828fde9718bbbb43a726167"
+dependencies = [
+ "gix-actor",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-utils",
+ "gix-validate",
+ "memmap2",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-refspec"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61755b27d57edc8940a1b1593c8c61548ca8e4c02da1ed8d5bfeda9eb2a6b761"
+dependencies = [
+ "bstr",
+ "gix-error",
+ "gix-glob",
+ "gix-hash",
+ "gix-revision",
+ "gix-validate",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-revision"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb5288fac706d3ea3e4e2ba9ec38b78743b8c02f422e18cb342299cfd6ab7e8"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-error",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "gix-trace",
+ "nonempty",
+]
+
+[[package]]
+name = "gix-revwalk"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "313813706b073a12ff7f9b2896bf3e6504cdac7cfbc97b1920114724705069f0"
+dependencies = [
+ "gix-commitgraph",
+ "gix-date",
+ "gix-error",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-sec"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5a3a2d3e504a238136751e646a6c028252286a0ea64ea9974bf0498633407c6"
+dependencies = [
+ "bitflags",
+ "gix-path",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "gix-shallow"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29187305521bfacf4aefd284ab28dbfa9fb74abd39a5e63dd313b1baa5808c27"
+dependencies = [
+ "bstr",
+ "gix-hash",
+ "gix-lock",
+ "nonempty",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-status"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68c6d2a8c521ffa205fe7e268c82e6d1378ba37cd826ca10ab6129fdc29a4b65"
+dependencies = [
+ "bstr",
+ "filetime",
+ "gix-diff",
+ "gix-dir",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-worktree",
+ "portable-atomic",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-submodule"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fd5fc8692890bd71a596e540fd4c364f8460eaa82c4eaaedebde6e1e3eb4d91"
+dependencies = [
+ "bstr",
+ "gix-config",
+ "gix-path",
+ "gix-pathspec",
+ "gix-refspec",
+ "gix-url",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-tempfile"
+version = "23.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb"
+dependencies = [
+ "dashmap",
+ "gix-fs",
+ "libc",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-registry",
+ "tempfile",
+]
+
+[[package]]
+name = "gix-trace"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e"
+
+[[package]]
+name = "gix-transport"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffd6a5c676b92d4ead5f5a2b2935024415dec69edc997b6090ca9cac010a3018"
+dependencies = [
+ "base64",
+ "bstr",
+ "gix-command",
+ "gix-credentials",
+ "gix-features",
+ "gix-packetline",
+ "gix-quote",
+ "gix-sec",
+ "gix-url",
+ "reqwest",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-traverse"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a14b7052c0786676c03e71fcfde7d7f0f8e8316e642b5cec6bb3998719b2ce5c"
+dependencies = [
+ "bitflags",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-url"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35842d099e813f6f6bba529e88d4670572149c3df79b7a412952259887721ece"
+dependencies = [
+ "bstr",
+ "gix-path",
+ "percent-encoding",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-utils"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c"
+dependencies = [
+ "bstr",
+ "fastrand",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "gix-validate"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5"
+dependencies = [
+ "bstr",
+]
+
+[[package]]
+name = "gix-worktree"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d69955eb5e2910832f88d041964b809eee01dadd579237e0b55efec58fd406fd"
+dependencies = [
+ "bstr",
+ "gix-attributes",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-validate",
+]
+
+[[package]]
+name = "gix-worktree-state"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a96dccbcf9e8fe0291c55f06e08da93ebb2e691c1311276f541eefcc6d70800"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-worktree",
+ "io-close",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-worktree-stream"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8444b8ed4662e1a0c97f3eceda29630001a1bbb2632201e50312623e594213"
+dependencies = [
+ "gix-attributes",
+ "gix-error",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-traverse",
+ "parking_lot",
+]
+
+[[package]]
 name = "globset"
 version = "0.4.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -639,6 +1902,25 @@
 ]
 
 [[package]]
+name = "h2"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
 name = "half"
 version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -650,6 +1932,15 @@
 ]
 
 [[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -668,6 +1959,17 @@
 
 [[package]]
 name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashbrown"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
@@ -678,6 +1980,16 @@
 ]
 
 [[package]]
+name = "heapless"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
 name = "heck"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -723,6 +2035,51 @@
 ]
 
 [[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "human_format"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b"
+
+[[package]]
 name = "hybrid-array"
 version = "0.4.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -732,6 +2089,65 @@
 ]
 
 [[package]]
+name = "hyper"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
 name = "icu_collections"
 version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -876,6 +2292,22 @@
 ]
 
 [[package]]
+name = "io-close"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
 name = "is_terminal_polyfill"
 version = "1.70.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -906,6 +2338,118 @@
 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
 
 [[package]]
+name = "jiff"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
+dependencies = [
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys",
+ "log",
+ "simd_cesu8",
+ "thiserror",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "jrb"
+version = "0.5.0-pre98"
+dependencies = [
+ "clap",
+ "jrsonnet-pkg",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
 name = "jrsonnet"
 version = "0.5.0-pre98"
 dependencies = [
@@ -1078,6 +2622,23 @@
 ]
 
 [[package]]
+name = "jrsonnet-pkg"
+version = "0.5.0-pre98"
+dependencies = [
+ "camino",
+ "directories",
+ "gix",
+ "peg",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "url",
+ "zip",
+]
+
+[[package]]
 name = "jrsonnet-rowan-parser"
 version = "0.5.0-pre98"
 dependencies = [
@@ -1109,7 +2670,7 @@
  "serde",
  "serde-saphyr",
  "serde_json",
- "sha1",
+ "sha1 0.11.0",
  "sha2",
  "sha3",
 ]
@@ -1158,10 +2719,25 @@
 checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
 dependencies = [
  "cfg-if",
- "cpufeatures",
+ "cpufeatures 0.3.0",
+]
+
+[[package]]
+name = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "static_assertions",
 ]
 
 [[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
 name = "leb128fmt"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1185,6 +2761,18 @@
 ]
 
 [[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "bitflags",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.5",
+]
+
+[[package]]
 name = "linux-raw-sys"
 version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1197,6 +2785,15 @@
 checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
 
 [[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
 name = "log"
 version = "0.4.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1244,6 +2841,32 @@
 ]
 
 [[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "maybe-async"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "md5"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1256,6 +2879,15 @@
 checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
 
 [[package]]
+name = "memmap2"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "mimalloc-sys"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1275,6 +2907,33 @@
 ]
 
 [[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "nix"
 version = "0.31.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1287,6 +2946,21 @@
 ]
 
 [[package]]
+name = "nonempty"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "num-bigint"
 version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1343,6 +3017,18 @@
 checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
 
 [[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
 name = "ouroboros"
 version = "0.18.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1377,6 +3063,29 @@
 ]
 
 [[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
 name = "pathdiff"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1416,6 +3125,18 @@
 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
 
 [[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
 name = "plotters"
 version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1444,6 +3165,21 @@
 ]
 
 [[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
 name = "potential_utf"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1494,6 +3230,17 @@
 ]
 
 [[package]]
+name = "prodash"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c"
+dependencies = [
+ "bytesize",
+ "human_format",
+ "parking_lot",
+]
+
+[[package]]
 name = "psm"
 version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1504,6 +3251,62 @@
 ]
 
 [[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand",
+ "ring",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
 name = "quote"
 version = "1.0.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1592,6 +3395,35 @@
 ]
 
 [[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
 name = "regex"
 version = "1.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1621,6 +3453,60 @@
 checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
 
 [[package]]
+name = "reqwest"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
 name = "rowan"
 version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1645,6 +3531,15 @@
 checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
 
 [[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
 name = "rustix"
 version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1654,10 +3549,85 @@
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
 ]
 
 [[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
 name = "rustversion"
 version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1673,6 +3643,44 @@
 ]
 
 [[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "semver"
 version = "1.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1740,13 +3748,34 @@
 
 [[package]]
 name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha1"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
 dependencies = [
  "cfg-if",
- "cpufeatures",
- "digest",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sha1-checked"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423"
+dependencies = [
+ "digest 0.10.7",
+ "sha1 0.10.6",
 ]
 
 [[package]]
@@ -1756,8 +3785,8 @@
 checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
 dependencies = [
  "cfg-if",
- "cpufeatures",
- "digest",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
 ]
 
 [[package]]
@@ -1766,29 +3795,102 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
 dependencies = [
- "digest",
+ "digest 0.11.3",
  "keccak",
 ]
 
 [[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
 name = "shlex"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
+name = "signal-hook"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "simd_cesu8"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
 name = "similar"
 version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
 
 [[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
 name = "smallvec"
 version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 
 [[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "stable_deref_trait"
 version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1804,7 +3906,7 @@
  "cfg-if",
  "libc",
  "psm",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1835,6 +3937,12 @@
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
 name = "syn"
 version = "2.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1857,6 +3965,15 @@
 ]
 
 [[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
 name = "synstructure"
 version = "0.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1877,7 +3994,7 @@
  "getrandom 0.4.2",
  "once_cell",
  "rustix",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1921,6 +4038,15 @@
 ]
 
 [[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
 name = "tinystr"
 version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1941,18 +4067,203 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28f0d049ccfaa566e14e9663d304d8577427b368cb4710a20528690287a738b"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typed-path"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
+
+[[package]]
 name = "typenum"
 version = "1.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
 
 [[package]]
+name = "uluru"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
 name = "ungrammar"
 version = "1.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a3e5df347f0bf3ec1d670aad6ca5c6a1859cd9ea61d2113125794654ccced68f"
 
 [[package]]
+name = "unicode-bom"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
+
+[[package]]
 name = "unicode-box-drawing"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1965,6 +4276,15 @@
 checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
 
 [[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
 name = "unicode-width"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1977,6 +4297,12 @@
 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
 [[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
 name = "url"
 version = "2.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2001,6 +4327,12 @@
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
 name = "version_check"
 version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2026,6 +4358,21 @@
 ]
 
 [[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
 name = "wasip2"
 version = "1.0.3+wasi-0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2146,6 +4493,25 @@
 ]
 
 [[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
 name = "winapi"
 version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2167,7 +4533,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2184,6 +4550,24 @@
 
 [[package]]
 name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
 version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
@@ -2192,6 +4576,135 @@
 ]
 
 [[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
 name = "wit-bindgen"
 version = "0.51.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2392,6 +4905,12 @@
 ]
 
 [[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
 name = "zerotrie"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2425,7 +4944,39 @@
 ]
 
 [[package]]
+name = "zip"
+version = "8.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
+dependencies = [
+ "crc32fast",
+ "flate2",
+ "indexmap",
+ "memchr",
+ "typed-path",
+ "zopfli",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
+[[package]]
 name = "zmij"
 version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@
 jrsonnet-types = { path = "./crates/jrsonnet-types", version = "0.5.0-pre98" }
 jrsonnet-formatter = { path = "./crates/jrsonnet-formatter", version = "0.5.0-pre98" }
 jrsonnet-lexer = { path = "./crates/jrsonnet-lexer", version = "0.5.0-pre98" }
+jrsonnet-pkg = { path = "./crates/jrsonnet-pkg", version = "0.5.0-pre98" }
 jrsonnet-gcmodule = { version = "0.5.0" }
 # Diagnostics.
 # hi-doc is my library, which handles text formatting very well, but isn't polished enough yet
@@ -116,6 +117,21 @@
 console_error_panic_hook = "0.1"
 getrandom = "0.3.4"
 
+# Bundler
+tracing = "0.1.44"
+tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+reqwest = { version = "0.13", features = [
+  "blocking",
+  "rustls",
+], default-features = false }
+zip = { version = "8", default-features = false, features = ["deflate"] }
+directories = "6.0.0"
+gix = { version = "0.83.0", features = [
+  "blocking-network-client",
+  "blocking-http-transport-reqwest-rust-tls",
+] }
+camino = { version = "1.2.2", features = ["serde1"] }
+
 [workspace.lints.rust]
 unsafe_op_in_unsafe_fn = "deny"
 
addedcmds/jrb/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/cmds/jrb/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "jrb"
+description = "jsonnet package manager"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+jrsonnet-pkg.workspace = true
+
+clap = { workspace = true, features = ["derive"] }
+serde = { workspace = true }
+serde_json.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
addedcmds/jrb/src/main.rsdiffbeforeafterboth

no changes

addedcrates/jrsonnet-pkg/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "jrsonnet-pkg"
+description = "jsonnet-bundler jsonnetfile parser and installer"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+thiserror.workspace = true
+tracing.workspace = true
+
+# Source url parser
+peg.workspace = true
+
+# Gix for git repos, reqwest + zip for github
+gix.workspace = true
+reqwest.workspace = true
+zip.workspace = true
+url.workspace = true
+camino.workspace = true
+
+# Global cache dir
+directories.workspace = true
addedcrates/jrsonnet-pkg/src/install/accessor.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/src/install/accessor.rs
@@ -0,0 +1,132 @@
+use std::{
+	fs::File,
+	io::{self, Read},
+	result,
+	str::FromStr as _,
+	sync::Mutex,
+};
+
+use tracing::warn;
+use zip::{ZipArchive, result::ZipError};
+
+use crate::jsonnet_bundler::{SubDir, SubDirEscapeError};
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+	#[error(transparent)]
+	Zip(#[from] ZipError),
+	#[error("invalid prefixed archive")]
+	ZipInvalidPrefix,
+	#[error("zip io: {0}")]
+	ZipIo(io::Error),
+	#[error("subdir not found: {0}")]
+	SubDirNotFound(SubDir),
+	#[error(transparent)]
+	SubdirEscape(#[from] SubDirEscapeError),
+}
+type Result<T, E = Error> = result::Result<T, E>;
+
+pub trait SourceAccessor {}
+
+pub struct ZipFileAccessor {
+	archive: Mutex<ZipArchive<File>>,
+	// Github archives have top-level directory with repo name
+	prefix: SubDir,
+}
+
+impl ZipFileAccessor {
+	pub fn new_prefixed(file: File) -> Result<Self> {
+		let archive = ZipArchive::new(file)?;
+		let prefix = archive.name_for_index(0).ok_or(Error::ZipInvalidPrefix)?;
+
+		Ok(Self {
+			prefix: SubDir::from_str(prefix)?,
+			archive: Mutex::new(archive),
+		})
+	}
+	/// Read a file from inside the archive's logical root (after stripping the
+	/// github-style `<repo>-<sha>/` prefix).
+	#[allow(clippy::significant_drop_tightening, reason = "false-positive")]
+	pub fn read(&self, name: &SubDir) -> Result<Option<Vec<u8>>> {
+		let prefixed = self
+			.prefix
+			.join(name)
+			.expect("prefix and name are both subdirs");
+		let mut archive = self.archive.lock().expect("not poisoned");
+		let mut v = match archive.by_name(prefixed.as_str()) {
+			Ok(v) => v,
+			Err(ZipError::FileNotFound) => return Ok(None),
+			Err(e) => return Err(e.into()),
+		};
+		if !v.is_file() {
+			return Ok(None);
+		}
+		let mut out = Vec::new();
+		v.read_to_end(&mut out).map_err(Error::ZipIo)?;
+		Ok(Some(out))
+	}
+	#[allow(clippy::significant_drop_tightening, reason = "false-positive")]
+	pub fn iter<E>(
+		&self,
+		subdir: &SubDir,
+		cb: &mut dyn FnMut(SubDir, AccessorEntry) -> Result<(), E>,
+	) -> Result<(), E>
+	where
+		E: From<Error>,
+	{
+		let mut archive = self.archive.lock().expect("not poisoned");
+		let len = archive.len();
+
+		let mut found = false;
+		for i in 0..len {
+			let mut entry = archive.by_index(i).map_err(Error::from)?;
+			let raw = entry.name();
+			let Ok(full_name) = SubDir::from_str(raw) else {
+				warn!("invalid zip entry name: {raw}");
+				continue;
+			};
+			// Peel off the github-archive top-level `<repo>-<sha>/` prefix.
+			let Some(in_repo) = full_name.strip_prefix(&self.prefix) else {
+				continue;
+			};
+			let Some(name) = in_repo.strip_prefix(subdir) else {
+				continue;
+			};
+			found = true;
+			if name.is_empty() && entry.is_dir() {
+				continue;
+			}
+
+			cb(
+				name.clone(),
+				if entry.is_dir() {
+					AccessorEntry::Dir
+				} else if entry.is_file() {
+					let mut data = Vec::new();
+					entry.read_to_end(&mut data).map_err(Error::ZipIo)?;
+					AccessorEntry::File(data)
+				} else {
+					// TODO: Symlinks?
+					panic!("unknown accessor entry type: {name:?}")
+				},
+			)?;
+		}
+
+		if !found {
+			return Err(Error::SubDirNotFound(subdir.clone()).into());
+		}
+
+		Ok(())
+	}
+	pub fn len(&self) -> usize {
+		self.archive.lock().expect("not poisoned").len()
+	}
+	pub fn is_empty(&self) -> bool {
+		self.len() == 0
+	}
+}
+
+pub enum AccessorEntry {
+	Dir,
+	File(Vec<u8>),
+}
addedcrates/jrsonnet-pkg/src/install/git.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/src/install/git.rs
@@ -0,0 +1,212 @@
+#![allow(clippy::result_large_err)]
+
+use std::{collections::HashSet, fs, path::Path};
+
+use gix::{
+	bstr::{self, ByteSlice},
+	interrupt, progress,
+	remote::{self, ref_map},
+};
+use tracing::info;
+
+use super::{Error, LocalExtraction, ResolveResult, Result, VendorSource, cache_dir};
+use crate::jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir};
+
+fn repo_cache_path(remote: &GitSource) -> Result<std::path::PathBuf> {
+	Ok(cache_dir("git")?.join(&remote.host).join(&remote.repo))
+}
+
+fn ensure_repo(remote: &GitSource) -> Result<gix::Repository> {
+	let cache_path = repo_cache_path(remote)?;
+
+	if cache_path.exists() {
+		if let Ok(repo) = gix::open(&cache_path) {
+			fetch_remote(&repo, &remote.remote())?;
+			return Ok(repo);
+		}
+		fs::remove_dir_all(&cache_path).map_err(|e| Error::Io(cache_path.clone(), e))?;
+	}
+
+	fs::create_dir_all(cache_path.parent().expect("has parent"))
+		.map_err(|e| Error::Io(cache_path.clone(), e))?;
+
+	let mut clone = gix::prepare_clone_bare(remote.remote(), &cache_path)?;
+	let (repo, _) = clone.fetch_only(progress::Discard, &interrupt::IS_INTERRUPTED)?;
+	fetch_remote(&repo, &remote.remote())?;
+
+	Ok(repo)
+}
+
+fn fetch_remote(repo: &gix::Repository, remote: &str) -> Result<(), Error> {
+	repo.remote_at(remote)?
+		.with_refspecs(["+refs/*:refs/*"], remote::Direction::Fetch)?
+		.connect(remote::Direction::Fetch)?
+		.prepare_fetch(progress::Discard, ref_map::Options::default())?
+		.receive(progress::Discard, &interrupt::IS_INTERRUPTED)?;
+	Ok(())
+}
+
+fn extract_tree(
+	repo: &gix::Repository,
+	tree: &gix::Tree<'_>,
+	subdir: &SubDir,
+	dest: &Path,
+) -> Result<(), Error> {
+	let target_tree;
+	let tree = if subdir.is_empty() {
+		tree
+	} else {
+		let mut t = tree.clone();
+		let entry = t
+			.peel_to_entry_by_path(subdir.as_path().as_std_path())?
+			.ok_or_else(|| Error::SubdirNotFound(subdir.to_string()))?;
+		target_tree = entry.object()?.into_tree();
+		&target_tree
+	};
+
+	let files = tree.traverse().breadthfirst.files()?;
+
+	for entry in &files {
+		if !entry.mode.is_blob() {
+			continue;
+		}
+		let rel_path = entry
+			.filepath
+			.to_str()
+			.map_err(|_| Error::InvalidPath(entry.filepath.to_string()))?;
+		let file_path = dest.join(rel_path);
+
+		if let Some(parent) = file_path.parent() {
+			fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;
+		}
+
+		let blob = repo.find_object(entry.oid)?;
+		fs::write(&file_path, &blob.data).map_err(|e| Error::Io(file_path, e))?;
+	}
+
+	Ok(())
+}
+
+fn resolve_version<'r>(repo: &'r gix::Repository, version: &str) -> Result<gix::Id<'r>> {
+	let spec: &bstr::BStr = version.into();
+	if let Ok(id) = repo.rev_parse_single(spec) {
+		return Ok(id);
+	}
+	for prefix in ["refs/heads/", "refs/tags/"] {
+		let refname = format!("{prefix}{version}");
+		if let Ok(r) = repo.find_reference(&refname) {
+			return Ok(r.into_fully_peeled_id()?);
+		}
+	}
+	Ok(repo.rev_parse_single(spec)?)
+}
+
+fn read_blob_at_path(
+	repo: &gix::Repository,
+	tree: &gix::Tree<'_>,
+	path: &SubDir,
+) -> Option<Vec<u8>> {
+	let mut t = tree.clone();
+	let entry = t
+		.peel_to_entry_by_path(path.as_path().as_std_path())
+		.ok()??;
+	let blob = repo.find_object(entry.oid()).ok()?;
+	Some(blob.data.clone())
+}
+
+fn collect_tree_deps(
+	repo: &gix::Repository,
+	tree: &gix::Tree<'_>,
+	dir: &SubDir,
+	git_deps: &mut Vec<Dependency>,
+	local_extractions: &mut Vec<LocalExtraction>,
+	visited: &mut HashSet<SubDir>,
+) {
+	if !visited.insert(dir.clone()) {
+		return;
+	}
+
+	let manifest_path = dir
+		.join("jsonnetfile.json")
+		.expect("appending a literal filename keeps it within parent");
+	let Some(data) = read_blob_at_path(repo, tree, &manifest_path) else {
+		return;
+	};
+	let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {
+		return;
+	};
+
+	for dep in manifest.dependencies {
+		match &dep.source {
+			Source::Git(_) => git_deps.push(dep),
+			Source::Local(local) => {
+				let Ok(child_dir) = local.resolve_under(dir) else {
+					info!("local source {local} escapes its package; skipping");
+					continue;
+				};
+				let name = child_dir
+					.file_name()
+					.map_or_else(|| local.to_string(), str::to_owned);
+				local_extractions.push(LocalExtraction {
+					tree_path: child_dir.clone(),
+					name,
+				});
+				collect_tree_deps(repo, tree, &child_dir, git_deps, local_extractions, visited);
+			}
+		}
+	}
+}
+
+pub(super) fn resolve(
+	git_source: &GitSource,
+	version: Option<&str>,
+) -> Result<ResolveResult, Error> {
+	info!("fetching via git: {}", git_source.remote());
+	let repo = ensure_repo(git_source)?;
+	let id = match version {
+		Some(v) => resolve_version(&repo, v)?,
+		None => repo.head_id()?,
+	};
+	let commit = repo.find_object(id)?.into_commit();
+	let tree = commit.tree()?;
+
+	let mut transitive_git_deps = Vec::new();
+	let mut local_extractions = Vec::new();
+	let mut visited = HashSet::new();
+	collect_tree_deps(
+		&repo,
+		&tree,
+		&git_source.subdir,
+		&mut transitive_git_deps,
+		&mut local_extractions,
+		&mut visited,
+	);
+
+	let repo_path = repo_cache_path(git_source)?;
+	let sha = id.to_string();
+
+	Ok(ResolveResult {
+		version: sha.clone(),
+		transitive_git_deps,
+		local_extractions,
+		source: VendorSource::GitTree {
+			repo_path,
+			commit_sha: sha,
+			subdir: git_source.subdir.clone(),
+		},
+	})
+}
+
+pub(super) fn extract(
+	repo_path: &Path,
+	commit_sha: &str,
+	subdir: &SubDir,
+	dest: &Path,
+) -> Result<(), Error> {
+	let repo = gix::open(repo_path)?;
+	let spec: &bstr::BStr = commit_sha.into();
+	let id = repo.rev_parse_single(spec)?;
+	let commit = repo.find_object(id)?.into_commit();
+	let tree = commit.tree()?;
+	extract_tree(&repo, &tree, subdir, dest)
+}
addedcrates/jrsonnet-pkg/src/install/github.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/src/install/github.rs
@@ -0,0 +1,190 @@
+#![allow(clippy::result_large_err)]
+
+use std::{
+	collections::HashSet,
+	fs::{self, File},
+	io::Write as _,
+	path::{Path, PathBuf},
+};
+
+use reqwest::{blocking::Response, header};
+use tracing::{debug, info};
+
+use super::{
+	Error, LocalExtraction, ResolveResult, Result, VendorSource,
+	accessor::{AccessorEntry, ZipFileAccessor},
+};
+use crate::{
+	install::{PKG_USER_AGENT, cache_dir},
+	jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir},
+};
+
+fn is_sha(s: &str) -> bool {
+	s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())
+}
+
+fn commit_cache_path(source: &GitSource, sha: &str) -> Result<PathBuf> {
+	Ok(cache_dir("github")?
+		.join(source.plain_repo_name())
+		.join(format!("{sha}.zip")))
+}
+
+fn resolve_sha(source: &GitSource, version: &str) -> Result<String> {
+	let url = format!(
+		"https://api.github.com/repos/{}/commits/{}",
+		source.plain_repo_name(),
+		version
+	);
+	let response = reqwest::blocking::Client::new()
+		.get(&url)
+		.header(header::ACCEPT, "application/vnd.github.sha")
+		.header(header::USER_AGENT, PKG_USER_AGENT)
+		.send()
+		.and_then(Response::error_for_status)?;
+	let sha = response.text()?;
+	Ok(sha.trim().to_owned())
+}
+
+fn fetch_zip(source: &GitSource, sha: &str) -> Result<ZipFileAccessor> {
+	let cached = commit_cache_path(source, sha)?;
+	if cached.exists() {
+		debug!("using cached archive {}", cached.display());
+		return Ok(ZipFileAccessor::new_prefixed(
+			File::open(&cached).map_err(|e| Error::Io(cached.clone(), e))?,
+		)?);
+	}
+
+	let url = format!(
+		"https://github.com/{}/archive/{}.zip",
+		source.plain_repo_name(),
+		sha
+	);
+	info!("downloading {url}");
+
+	let bytes = reqwest::blocking::Client::new()
+		.get(&url)
+		.header(header::USER_AGENT, PKG_USER_AGENT)
+		.send()
+		.and_then(Response::error_for_status)?
+		.bytes()?;
+
+	if let Some(parent) = cached.parent() {
+		fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;
+	}
+	let mut downloaded = File::create_new(&cached).map_err(|e| Error::Io(cached.clone(), e))?;
+	downloaded
+		.write_all(&bytes)
+		.map_err(|e| Error::Io(cached.clone(), e))?;
+
+	Ok(ZipFileAccessor::new_prefixed(downloaded)?)
+}
+
+fn open_cached_zip(zip_path: &Path) -> Result<ZipFileAccessor> {
+	Ok(ZipFileAccessor::new_prefixed(
+		File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,
+	)?)
+}
+
+fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {
+	archive.iter(subdir, &mut |name, entry| {
+		let target = dest.join(name);
+		match entry {
+			AccessorEntry::Dir => {
+				fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;
+			}
+			AccessorEntry::File(data) => {
+				if let Some(parent) = target.parent() {
+					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;
+				}
+				fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;
+			}
+		}
+		Ok(())
+	})
+}
+
+fn collect_archive_deps(
+	archive: &ZipFileAccessor,
+	dir: &SubDir,
+	git_deps: &mut Vec<Dependency>,
+	local_extractions: &mut Vec<LocalExtraction>,
+	visited: &mut HashSet<SubDir>,
+) -> Result<()> {
+	if !visited.insert(dir.clone()) {
+		return Ok(());
+	}
+
+	let manifest_path = dir
+		.join("jsonnetfile.json")
+		.expect("appending a literal filename keeps it within parent");
+
+	let Some(data) = archive.read(&manifest_path)? else {
+		return Ok(());
+	};
+	let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {
+		return Ok(());
+	};
+
+	for dep in manifest.dependencies {
+		match &dep.source {
+			Source::Git(_) => git_deps.push(dep),
+			Source::Local(local) => {
+				let Ok(child_dir) = local.resolve_under(dir) else {
+					tracing::info!("local source {local} escapes its package; skipping");
+					continue;
+				};
+				let name = child_dir
+					.file_name()
+					.map_or_else(|| local.to_string(), str::to_owned);
+				local_extractions.push(LocalExtraction {
+					tree_path: child_dir.clone(),
+					name,
+				});
+				collect_archive_deps(archive, &child_dir, git_deps, local_extractions, visited)?;
+			}
+		}
+	}
+	Ok(())
+}
+
+pub(super) fn resolve(source: &GitSource, version: Option<&str>) -> Result<ResolveResult> {
+	let version_str = version.unwrap_or("HEAD");
+	let sha = if is_sha(version_str) {
+		version_str.to_owned()
+	} else {
+		let resolved = resolve_sha(source, version_str)?;
+		info!("resolved {version_str} to {resolved}");
+		resolved
+	};
+
+	let archive = fetch_zip(source, &sha)?;
+
+	let mut transitive_git_deps = Vec::new();
+	let mut local_extractions = Vec::new();
+	let mut visited = HashSet::new();
+	collect_archive_deps(
+		&archive,
+		&source.subdir,
+		&mut transitive_git_deps,
+		&mut local_extractions,
+		&mut visited,
+	)?;
+
+	let zip_path = commit_cache_path(source, &sha)?;
+
+	Ok(ResolveResult {
+		version: sha.clone(),
+		transitive_git_deps,
+		local_extractions,
+		source: VendorSource::GithubZip {
+			zip_path,
+			commit_sha: sha,
+			subdir: source.subdir.clone(),
+		},
+	})
+}
+
+pub(super) fn extract(zip_path: &Path, subdir: &SubDir, dest: &Path) -> Result<()> {
+	let archive = open_cached_zip(zip_path)?;
+	extract_subdir(&archive, subdir, dest)
+}
addedcrates/jrsonnet-pkg/src/install/mod.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/src/install/mod.rs
@@ -0,0 +1,406 @@
+#![allow(clippy::result_large_err)]
+
+pub mod accessor;
+mod git;
+mod github;
+
+use std::{
+	collections::{BTreeMap, HashSet},
+	fs,
+	path::{Path, PathBuf},
+	result,
+};
+
+use camino::Utf8PathBuf;
+use tracing::info;
+
+use crate::jsonnet_bundler::{Dependency, GitScheme, GitSource, JsonnetFile, Source, SubDir};
+
+pub const PKG_USER_AGENT: &str = "jrsonnet-pkg (https://delta.rocks/jrsonnet)";
+
+pub fn cache_dir(subdir: &str) -> Result<std::path::PathBuf> {
+	Ok(directories::ProjectDirs::from("rocks", "delta", "jrsonnet")
+		.ok_or(Error::XdgUnavailable)?
+		.cache_dir()
+		.join(subdir))
+}
+
+pub(crate) struct LocalExtraction {
+	/// Path inside the parent repo's tree where this local source lives.
+	pub tree_path: SubDir,
+	pub name: String,
+}
+
+pub(crate) struct ResolveResult {
+	pub version: String,
+	pub transitive_git_deps: Vec<Dependency>,
+	pub local_extractions: Vec<LocalExtraction>,
+	pub source: VendorSource,
+}
+
+const VERSION_FILE: &str = ".version";
+
+/// How to populate a vendor path.
+pub enum VendorSource {
+	GitTree {
+		repo_path: PathBuf,
+		commit_sha: String,
+		subdir: SubDir,
+	},
+	GithubZip {
+		zip_path: PathBuf,
+		commit_sha: String,
+		subdir: SubDir,
+	},
+	Symlink(Utf8PathBuf),
+}
+
+impl VendorSource {
+	fn with_subdir(&self, new_subdir: SubDir) -> Self {
+		match self {
+			VendorSource::GitTree {
+				repo_path,
+				commit_sha,
+				..
+			} => VendorSource::GitTree {
+				repo_path: repo_path.clone(),
+				commit_sha: commit_sha.clone(),
+				subdir: new_subdir,
+			},
+			VendorSource::GithubZip {
+				zip_path,
+				commit_sha,
+				..
+			} => VendorSource::GithubZip {
+				zip_path: zip_path.clone(),
+				commit_sha: commit_sha.clone(),
+				subdir: new_subdir,
+			},
+			VendorSource::Symlink(target) => VendorSource::Symlink(target.clone()),
+		}
+	}
+}
+
+pub struct InstallPlan {
+	pub lock: JsonnetFile,
+	/// vendor-relative path -> how to obtain it.
+	pub entries: BTreeMap<Utf8PathBuf, VendorSource>,
+}
+
+pub fn install(
+	manifest: &JsonnetFile,
+	lock: Option<&JsonnetFile>,
+	vendor_dir: &Path,
+	dry_run: bool,
+) -> Result<JsonnetFile, Error> {
+	let plan = resolve(manifest, lock)?;
+	execute(&plan, vendor_dir, dry_run)?;
+	Ok(plan.lock)
+}
+
+pub fn resolve(manifest: &JsonnetFile, lock: Option<&JsonnetFile>) -> Result<InstallPlan, Error> {
+	let mut plan = InstallPlan {
+		lock: JsonnetFile {
+			version: manifest.version,
+			dependencies: Vec::new(),
+			legacy_imports: manifest.legacy_imports,
+		},
+		entries: BTreeMap::new(),
+	};
+	let mut installed = HashSet::new();
+
+	resolve_deps(
+		&manifest.dependencies,
+		lock,
+		manifest.legacy_imports,
+		&mut plan,
+		&mut installed,
+	)?;
+
+	Ok(plan)
+}
+
+fn is_up_to_date(dest: &Path, version: &str) -> bool {
+	fs::read_to_string(dest.join(VERSION_FILE)).is_ok_and(|v| v.trim() == version)
+}
+
+fn write_version(dest: &Path, version: &str) -> Result<(), Error> {
+	fs::write(dest.join(VERSION_FILE), format!("{version}\n"))
+		.map_err(|e| Error::Io(dest.join(VERSION_FILE), e))
+}
+
+pub fn execute(plan: &InstallPlan, vendor_dir: &Path, dry_run: bool) -> Result<(), Error> {
+	if !dry_run {
+		for (path, source) in &plan.entries {
+			let dest = vendor_dir.join(path);
+			match source {
+				VendorSource::GitTree {
+					repo_path,
+					commit_sha,
+					subdir,
+				} => {
+					if is_up_to_date(&dest, commit_sha) {
+						continue;
+					}
+					info!("extract {path}");
+					if dest.exists() {
+						fs::remove_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;
+					}
+					fs::create_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;
+					git::extract(repo_path, commit_sha, subdir, &dest)?;
+					write_version(&dest, commit_sha)?;
+				}
+				VendorSource::GithubZip {
+					zip_path,
+					commit_sha,
+					subdir,
+				} => {
+					if is_up_to_date(&dest, commit_sha) {
+						continue;
+					}
+					info!("extract {path}");
+					if dest.exists() {
+						fs::remove_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;
+					}
+					fs::create_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;
+					github::extract(zip_path, subdir, &dest)?;
+					write_version(&dest, commit_sha)?;
+				}
+				VendorSource::Symlink(_) => {}
+			}
+		}
+		for (path, source) in &plan.entries {
+			if let VendorSource::Symlink(target) = source {
+				let dest = vendor_dir.join(path);
+				if dest
+					.symlink_metadata()
+					.is_ok_and(|m| m.file_type().is_symlink())
+				{
+					if fs::read_link(&dest).is_ok_and(|t| t == target.as_std_path()) {
+						continue;
+					}
+					fs::remove_file(&dest).map_err(|e| Error::Io(dest.clone(), e))?;
+				}
+				info!("symlink {path} -> {target}");
+				std::os::unix::fs::symlink(target.as_std_path(), &dest)
+					.map_err(|e| Error::Io(dest.clone(), e))?;
+			}
+		}
+	}
+	prune(plan, vendor_dir, dry_run)?;
+	Ok(())
+}
+
+fn prune(plan: &InstallPlan, vendor_dir: &Path, dry_run: bool) -> Result<(), Error> {
+	if !vendor_dir.is_dir() {
+		return Ok(());
+	}
+	prune_recursive(plan, vendor_dir, vendor_dir, dry_run)
+}
+
+fn prune_recursive(
+	plan: &InstallPlan,
+	vendor_dir: &Path,
+	dir: &Path,
+	dry_run: bool,
+) -> Result<(), Error> {
+	let entries = fs::read_dir(dir).map_err(|e| Error::Io(dir.to_owned(), e))?;
+	for entry in entries {
+		let entry = entry.map_err(|e| Error::Io(dir.to_owned(), e))?;
+		let path = entry.path();
+		let rel = path
+			.strip_prefix(vendor_dir)
+			.expect("path is under vendor_dir");
+		let Ok(rel) = Utf8PathBuf::try_from(rel.to_owned()) else {
+			info!("prune (non-utf8) {}", rel.display());
+			continue;
+		};
+
+		if plan.entries.contains_key(&rel) {
+			continue;
+		}
+
+		let ft = entry.file_type().map_err(|e| Error::Io(path.clone(), e))?;
+		if ft.is_symlink() {
+			info!("prune {rel}");
+			if !dry_run {
+				fs::remove_file(&path).map_err(|e| Error::Io(path, e))?;
+			}
+		} else if ft.is_dir() {
+			let prefix: Utf8PathBuf = format!("{rel}/").into();
+			let has_descendants = plan
+				.entries
+				.range(prefix.clone()..)
+				.next()
+				.is_some_and(|(k, _)| k.starts_with(&prefix));
+			if has_descendants {
+				prune_recursive(plan, vendor_dir, &path, dry_run)?;
+			} else {
+				info!("prune {rel}");
+				if !dry_run {
+					fs::remove_dir_all(&path).map_err(|e| Error::Io(path, e))?;
+				}
+			}
+		} else {
+			info!("prune {rel}");
+			if !dry_run {
+				fs::remove_file(&path).map_err(|e| Error::Io(path, e))?;
+			}
+		}
+	}
+
+	if !dry_run
+		&& dir != vendor_dir
+		&& let Ok(mut entries) = fs::read_dir(dir)
+		&& entries.next().is_none()
+	{
+		let _ = fs::remove_dir(dir);
+	}
+
+	Ok(())
+}
+
+fn resolve_one(git_source: &GitSource, version: Option<&str>) -> Result<ResolveResult, Error> {
+	if git_source.host == "github.com" && git_source.scheme == GitScheme::Https {
+		match github::resolve(git_source, version) {
+			Ok(result) => return Ok(result),
+			Err(e) => {
+				info!("github archive failed ({e}), falling back to git");
+			}
+		}
+	}
+	git::resolve(git_source, version)
+}
+
+fn locked_version<'a>(dep: &Dependency, lock: Option<&'a JsonnetFile>) -> Option<&'a str> {
+	let lock = lock?;
+	let key = dep.canonical_name();
+	lock.dependencies
+		.iter()
+		.find(|d| d.canonical_name() == key)
+		.and_then(|d| d.version.as_deref())
+}
+
+fn resolve_deps(
+	deps: &[Dependency],
+	lock: Option<&JsonnetFile>,
+	legacy_imports: bool,
+	plan: &mut InstallPlan,
+	installed: &mut HashSet<Utf8PathBuf>,
+) -> Result<(), Error> {
+	for dep in deps {
+		let Source::Git(git_source) = &dep.source else {
+			continue;
+		};
+
+		let canonical = dep.canonical_name();
+		if !installed.insert(canonical.clone()) {
+			continue;
+		}
+
+		let version = locked_version(dep, lock).or(dep.version.as_deref());
+
+		info!(
+			"resolving {canonical} (version: {})",
+			version.unwrap_or("<TBD>")
+		);
+
+		let result = resolve_one(git_source, version)?;
+
+		plan.lock.dependencies.push(Dependency {
+			source: dep.source.clone(),
+			version: Some(result.version),
+			sum: dep.sum.clone(),
+			name: dep.name.clone(),
+			single: dep.single,
+		});
+
+		let mut repo_base = Utf8PathBuf::from(git_source.host.as_str());
+		repo_base.push(git_source.plain_repo_name());
+
+		// Legacy symlink for the dep. Skipped if `legacyImports: false`, unless
+		// the user explicitly set `dep.name` (which is always honored).
+		if legacy_imports || dep.name.is_some() {
+			let legacy = Utf8PathBuf::from(dep.legacy_link_name());
+			if legacy != canonical {
+				plan.entries
+					.insert(legacy, VendorSource::Symlink(canonical.clone()));
+			}
+		}
+
+		for extraction in &result.local_extractions {
+			let extraction_canonical = repo_base.join(&extraction.tree_path);
+			plan.entries.insert(
+				extraction_canonical.clone(),
+				result.source.with_subdir(extraction.tree_path.clone()),
+			);
+			if legacy_imports {
+				let extraction_name = Utf8PathBuf::from(&extraction.name);
+				if extraction_name != extraction_canonical {
+					plan.entries
+						.insert(extraction_name, VendorSource::Symlink(extraction_canonical));
+				}
+			}
+		}
+
+		// Main entry (after local extractions used with_subdir)
+		plan.entries.insert(canonical, result.source);
+
+		resolve_deps(
+			&result.transitive_git_deps,
+			lock,
+			legacy_imports,
+			plan,
+			installed,
+		)?;
+	}
+
+	Ok(())
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+	#[error("io error for {0}: {1}")]
+	Io(PathBuf, std::io::Error),
+	#[error("failed to discover xdg directories")]
+	XdgUnavailable,
+	#[error("git clone failed: {0}")]
+	GitClone(#[from] gix::clone::Error),
+	#[error(transparent)]
+	GitRemote(#[from] gix::remote::init::Error),
+	#[error(transparent)]
+	GitConnect(#[from] gix::remote::connect::Error),
+	#[error(transparent)]
+	GitFetchPrepare(#[from] gix::remote::fetch::prepare::Error),
+	#[error(transparent)]
+	GitRemoteFetch(#[from] gix::remote::fetch::Error),
+	#[error(transparent)]
+	GitCloneFetch(#[from] gix::clone::fetch::Error),
+	#[error(transparent)]
+	GitFindObject(#[from] gix::object::find::existing::Error),
+	#[error(transparent)]
+	GitTraverse(#[from] gix::traverse::tree::breadthfirst::Error),
+	#[error(transparent)]
+	GitHead(#[from] gix::reference::head_id::Error),
+	#[error(transparent)]
+	GitCommit(#[from] gix::object::commit::Error),
+	#[error(transparent)]
+	GitRevparse(#[from] gix::revision::spec::parse::single::Error),
+	#[error(transparent)]
+	GitRefspec(#[from] gix::refspec::parse::Error),
+	#[error(transparent)]
+	GitPeel(#[from] gix::reference::peel::Error),
+	#[error(transparent)]
+	GitOpen(#[from] gix::open::Error),
+	#[error("http error: {0}")]
+	Http(#[from] reqwest::Error),
+	#[error("zip error: {0}")]
+	Zip(Box<zip::result::ZipError>),
+	#[error(transparent)]
+	Accessor(#[from] accessor::Error),
+	#[error("unknown subdir: {0}")]
+	SubdirNotFound(String),
+	#[error("invalid path in tree: {0}")]
+	InvalidPath(String),
+}
+pub(crate) type Result<T, E = Error> = result::Result<T, E>;
addedcrates/jrsonnet-pkg/src/jsonnet_bundler.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/src/jsonnet_bundler.rs
@@ -0,0 +1,883 @@
+use std::{fmt, path::Path, str::FromStr};
+
+use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
+use serde::{Deserialize, Serialize, de};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct JsonnetFile {
+	pub version: u32,
+	#[serde(default)]
+	pub dependencies: Vec<Dependency>,
+	#[serde(default = "legacy_imports_default", rename = "legacyImports")]
+	pub legacy_imports: bool,
+}
+
+fn legacy_imports_default() -> bool {
+	true
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Dependency {
+	pub source: Source,
+	#[serde(default, skip_serializing_if = "Option::is_none")]
+	pub version: Option<String>,
+	#[serde(default, skip_serializing_if = "Option::is_none")]
+	pub sum: Option<String>,
+	#[serde(default, skip_serializing_if = "Option::is_none")]
+	pub name: Option<String>,
+	#[serde(default, skip_serializing_if = "is_false")]
+	pub single: bool,
+}
+
+#[allow(clippy::trivially_copy_pass_by_ref, reason = "serde")]
+fn is_false(v: &bool) -> bool {
+	!v
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Source {
+	Git(GitSource),
+	Local(LocalSource),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum GitScheme {
+	Https,
+	Ssh,
+}
+
+/// Wrapper over `Utf8PathBuf`, ensuring it can't escape to either an absolute
+/// path or a parent directory.
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct SubDir(Utf8PathBuf);
+
+#[derive(Debug, thiserror::Error)]
+#[error("subdir attempted to escape")]
+pub struct SubDirEscapeError;
+
+impl FromStr for SubDir {
+	type Err = SubDirEscapeError;
+	fn from_str(s: &str) -> Result<Self, Self::Err> {
+		Self::try_from(Utf8PathBuf::from(s))
+	}
+}
+impl TryFrom<Utf8PathBuf> for SubDir {
+	type Error = SubDirEscapeError;
+
+	fn try_from(buf: Utf8PathBuf) -> Result<Self, Self::Error> {
+		for ele in buf.components() {
+			match ele {
+				Utf8Component::Prefix(_) | Utf8Component::RootDir | Utf8Component::ParentDir => {
+					return Err(SubDirEscapeError);
+				}
+				Utf8Component::CurDir | Utf8Component::Normal(_) => {}
+			}
+		}
+		Ok(Self(buf))
+	}
+}
+
+impl SubDir {
+	pub fn empty() -> Self {
+		Self(Utf8PathBuf::new())
+	}
+	pub fn as_str(&self) -> &str {
+		self.0.as_str()
+	}
+	pub fn as_path(&self) -> &Utf8Path {
+		&self.0
+	}
+	pub fn into_inner(self) -> Utf8PathBuf {
+		self.0
+	}
+	pub fn join(&self, other: impl AsRef<Utf8Path>) -> Result<SubDir, SubDirEscapeError> {
+		SubDir::try_from(self.0.join(other))
+	}
+	pub fn strip_prefix(&self, prefix: &SubDir) -> Option<SubDir> {
+		Some(
+			SubDir::try_from(self.0.strip_prefix(&prefix.0).ok()?.to_owned())
+				.expect("stripping would not result in escape"),
+		)
+	}
+	pub fn is_empty(&self) -> bool {
+		self.0.as_str().is_empty()
+	}
+	pub fn file_name(&self) -> Option<&str> {
+		self.0.file_name()
+	}
+	/// Strip a trailing `.git` extension, if any.
+	#[must_use]
+	pub fn without_git_suffix(&self) -> SubDir {
+		let mut p = self.0.clone();
+		if p.extension() == Some("git") {
+			p.set_extension("");
+		}
+		SubDir(p)
+	}
+}
+impl AsRef<Utf8Path> for SubDir {
+	fn as_ref(&self) -> &Utf8Path {
+		&self.0
+	}
+}
+impl AsRef<Path> for SubDir {
+	fn as_ref(&self) -> &Path {
+		self.0.as_ref()
+	}
+}
+impl AsRef<str> for SubDir {
+	fn as_ref(&self) -> &str {
+		self.0.as_str()
+	}
+}
+impl fmt::Display for SubDir {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}", self.0)
+	}
+}
+impl PartialEq<str> for SubDir {
+	fn eq(&self, other: &str) -> bool {
+		self.0.as_str() == other
+	}
+}
+impl PartialEq<&str> for SubDir {
+	fn eq(&self, other: &&str) -> bool {
+		self.0.as_str() == *other
+	}
+}
+
+/// Wrapper over `String`, guaranteeing the value is a valid host: only ASCII
+/// alphanumerics, dashes and dots, with at least one segment.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Hostname(String);
+
+#[derive(Debug, thiserror::Error)]
+#[error("invalid hostname")]
+pub struct InvalidHostnameError;
+
+impl FromStr for Hostname {
+	type Err = InvalidHostnameError;
+
+	fn from_str(s: &str) -> Result<Self, Self::Err> {
+		if s.is_empty() || s == "." || s == ".." {
+			return Err(InvalidHostnameError);
+		}
+		for seg in s.split('.') {
+			if seg.is_empty() {
+				return Err(InvalidHostnameError);
+			}
+			if !seg.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
+				return Err(InvalidHostnameError);
+			}
+		}
+		Ok(Self(s.to_owned()))
+	}
+}
+
+impl Hostname {
+	pub fn as_str(&self) -> &str {
+		&self.0
+	}
+}
+impl AsRef<str> for Hostname {
+	fn as_ref(&self) -> &str {
+		&self.0
+	}
+}
+impl AsRef<Path> for Hostname {
+	fn as_ref(&self) -> &Path {
+		self.0.as_ref()
+	}
+}
+impl AsRef<Utf8Path> for Hostname {
+	fn as_ref(&self) -> &Utf8Path {
+		self.0.as_str().into()
+	}
+}
+impl fmt::Display for Hostname {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		f.write_str(&self.0)
+	}
+}
+impl PartialEq<str> for Hostname {
+	fn eq(&self, other: &str) -> bool {
+		self.0 == other
+	}
+}
+impl PartialEq<&str> for Hostname {
+	fn eq(&self, other: &&str) -> bool {
+		self.0 == *other
+	}
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct GitSource {
+	pub scheme: GitScheme,
+	pub host: Hostname,
+	/// Repo path relative to host: `user/repo[.git]` (or with subgroups).
+	pub repo: SubDir,
+	/// Subdirectory within the repo. Empty means the repo root.
+	pub subdir: SubDir,
+}
+
+/// A relative path that may climb out of its package via `..` parts, but only
+/// at the head - once you go down (`SubDir` portion) you can't go back up.
+///
+/// The total upward count is bounded only at resolution time, against the
+/// containing package's depth.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct LocalSource {
+	pub ups: usize,
+	pub dir: SubDir,
+}
+
+impl FromStr for LocalSource {
+	// Technically incorrect, as it only rejects mid-path ../'s...
+	type Err = SubDirEscapeError;
+
+	fn from_str(s: &str) -> Result<Self, Self::Err> {
+		let mut ups = 0usize;
+		let mut rest = s;
+		loop {
+			if let Some(r) = rest.strip_prefix("./") {
+				rest = r;
+			} else if rest == "." {
+				rest = "";
+				break;
+			} else if let Some(r) = rest.strip_prefix("../") {
+				ups = ups.checked_add(1).expect("can't be longer than s length");
+				rest = r;
+			} else if rest == ".." {
+				ups = ups.checked_add(1).expect("can't be longer than s length");
+				rest = "";
+				break;
+			} else {
+				break;
+			}
+		}
+		Ok(Self {
+			ups,
+			dir: SubDir::from_str(rest)?,
+		})
+	}
+}
+
+impl fmt::Display for LocalSource {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		let mut out = String::with_capacity(self.ups * 3 + self.dir.as_str().len());
+		for _ in 0..self.ups {
+			out.push_str("../");
+		}
+		out.push_str(self.dir.as_str());
+		if out.is_empty() {
+			out.push('.');
+		} else if out.ends_with('/') {
+			out.pop();
+		}
+		// TODO: I didn't finish
+		f.write_str(&out)
+	}
+}
+
+impl LocalSource {
+	pub fn resolve_under(&self, parent: &SubDir) -> Result<SubDir, SubDirEscapeError> {
+		let mut comps: Vec<&str> = parent.as_path().components().map(|c| c.as_str()).collect();
+		if self.ups > comps.len() {
+			return Err(SubDirEscapeError);
+		}
+		comps.truncate(comps.len() - self.ups);
+		let mut buf = Utf8PathBuf::from_iter(comps);
+		buf.push(self.dir.as_path());
+		SubDir::try_from(buf)
+	}
+}
+
+impl Serialize for LocalSource {
+	fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
+		#[derive(Serialize)]
+		struct JsonLocal<'a> {
+			directory: &'a str,
+		}
+		let rendered = self.to_string();
+		JsonLocal {
+			directory: &rendered,
+		}
+		.serialize(ser)
+	}
+}
+
+impl<'de> Deserialize<'de> for LocalSource {
+	fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
+		#[derive(Deserialize)]
+		struct JsonLocal {
+			directory: String,
+		}
+		let j = JsonLocal::deserialize(de)?;
+		LocalSource::from_str(&j.directory)
+			.map_err(|e| de::Error::custom(format!("invalid local path {:?}: {e}", j.directory)))
+	}
+}
+
+impl GitSource {
+	/// Repo path with the trailing `.git` (if any) stripped.
+	pub fn plain_repo_name(&self) -> SubDir {
+		self.repo.without_git_suffix()
+	}
+
+	/// Canonical install path: `host/user/repo[/subdir]`.
+	pub fn name(&self) -> SubDir {
+		let mut p = Utf8PathBuf::from(self.host.as_str());
+		p.push(self.plain_repo_name());
+		if !self.subdir.is_empty() {
+			p.push(self.subdir.as_path());
+		}
+		SubDir::try_from(p).expect("host + subdirs is a valid SubDir")
+	}
+
+	/// Last path component of `repo[/subdir]`, used as the legacy symlink name.
+	pub fn legacy_name(&self) -> String {
+		self.name()
+			.file_name()
+			.expect("name has at least one component")
+			.to_owned()
+	}
+
+	/// Git remote URL for cloning.
+	pub fn remote(&self) -> String {
+		let host = self.host.as_str();
+		let repo = self.repo.as_str();
+		match self.scheme {
+			GitScheme::Ssh => format!("ssh://git@{host}/{repo}"),
+			GitScheme::Https => format!("https://{host}/{repo}"),
+		}
+	}
+
+	/// Parse a URI like `github.com/user/repo/subdir@version` into a
+	/// `Dependency`.
+	pub fn parse(uri: &str) -> Option<Dependency> {
+		git_uri::parse(uri).ok()
+	}
+}
+
+peg::parser! {
+	grammar git_uri() for str {
+		rule host_segment() = ['a'..='z' | 'A'..='Z' | '0'..='9' | '-']+;
+		rule host() -> Hostname
+			= s:$(host_segment()++".")
+			{ Hostname::from_str(s).expect("grammar restricted to valid host chars") }
+
+		// User/repo path segments. `~` is allowed for Bitbucket personal repos.
+		rule path_segment() = ['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '~']+;
+		// Subdir segments allow dots (e.g. `ksonnet.beta.3`).
+		rule subdir_segment() = ['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+;
+
+		// `user[/group...]/repo.git`
+		rule repo_dotgit() -> SubDir
+			= s:$(path_segment()++"/" ".git")
+			{ SubDir::from_str(s).expect("grammar restricted to subpath chars") }
+		// `user/repo` (exactly two segments, no `.git`)
+		rule repo_simple() -> SubDir
+			= s:$(path_segment() "/" path_segment())
+			{ SubDir::from_str(s).expect("grammar restricted to subpath chars") }
+
+		// Subdir starts with `/`. May be empty.
+		rule subdir() -> SubDir
+			= "/" s:$(subdir_segment() ** "/") "/"?
+			{ SubDir::from_str(s).expect("grammar restricted to subdir chars") }
+			/ { SubDir::empty() }
+
+		rule version() -> &'input str
+			= "@" v:$([_]+) { v }
+
+
+		// git@host:path.git[/subdir][@version]  (SCP style)
+		rule scp_uri() -> Dependency
+			= "git@" h:host() ":" repo:repo_dotgit() subdir:subdir()
+			  v:version()?
+		{
+			make_dep(GitScheme::Ssh, h, repo, subdir, v)
+		}
+
+		// ssh://git@host/path.git[/subdir][@version]
+		rule ssh_uri() -> Dependency
+			= "ssh://git@" h:host() "/" repo:repo_dotgit() subdir:subdir()
+			  v:version()?
+		{
+			make_dep(GitScheme::Ssh, h, repo, subdir, v)
+		}
+
+		// [https://]host/path.git[/subdir][@version]
+		rule https_dotgit() -> Dependency
+			= "https://"? h:host() "/" repo:repo_dotgit() subdir:subdir()
+			  v:version()?
+		{
+			make_dep(GitScheme::Https, h, repo, subdir, v)
+		}
+
+		// [https://]host/user/repo[/subdir[/...]][@version]
+		rule https_simple() -> Dependency
+			= "https://"? h:host() "/" repo:repo_simple() subdir:subdir()
+			  v:version()?
+		{
+			make_dep(GitScheme::Https, h, repo, subdir, v)
+		}
+
+		pub rule parse() -> Dependency
+			= ssh_uri() / scp_uri() / https_dotgit() / https_simple()
+	}
+}
+
+fn make_dep(
+	scheme: GitScheme,
+	host: Hostname,
+	repo: SubDir,
+	subdir: SubDir,
+	version: Option<&str>,
+) -> Dependency {
+	Dependency {
+		source: Source::Git(GitSource {
+			scheme,
+			host,
+			repo,
+			subdir,
+		}),
+		version: version.map(str::to_owned),
+		sum: None,
+		name: None,
+		single: false,
+	}
+}
+
+impl Dependency {
+	/// Canonical install path for deduplication and vendor extraction.
+	pub fn canonical_name(&self) -> Utf8PathBuf {
+		match &self.source {
+			Source::Git(git) => git.name().into_inner(),
+			Source::Local(local) => Utf8PathBuf::from(local.to_string()),
+		}
+	}
+
+	/// Legacy symlink name: `dep.name` override, or last path component.
+	pub fn legacy_link_name(&self) -> String {
+		if let Some(name) = &self.name {
+			return name.clone();
+		}
+		match &self.source {
+			Source::Git(git) => git.legacy_name(),
+			Source::Local(local) => local
+				.dir
+				.file_name()
+				.map_or_else(|| local.to_string(), str::to_owned),
+		}
+	}
+}
+
+impl Serialize for GitSource {
+	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+		#[derive(Serialize)]
+		struct JsonGit<'a> {
+			remote: String,
+			#[serde(skip_serializing_if = "str::is_empty")]
+			subdir: &'a str,
+		}
+		JsonGit {
+			remote: self.remote(),
+			subdir: self.subdir.as_str(),
+		}
+		.serialize(serializer)
+	}
+}
+
+impl<'de> Deserialize<'de> for GitSource {
+	fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+		#[derive(Deserialize)]
+		struct JsonGit {
+			remote: String,
+			#[serde(default)]
+			subdir: String,
+		}
+		let j = JsonGit::deserialize(deserializer)?;
+
+		let parsed = GitSource::parse(&j.remote)
+			.ok_or_else(|| de::Error::custom(format!("unable to parse git url {:?}", j.remote)))?;
+		let Source::Git(mut gs) = parsed.source else {
+			unreachable!()
+		};
+
+		if !j.subdir.is_empty() {
+			gs.subdir = SubDir::from_str(j.subdir.trim_start_matches('/'))
+				.map_err(|e| de::Error::custom(format!("invalid subdir {:?}: {e}", j.subdir)))?;
+		}
+
+		Ok(gs)
+	}
+}
+
+impl JsonnetFile {
+	pub fn load(path: &Path) -> Result<Self, Error> {
+		let data = std::fs::read(path).map_err(|e| Error::Io(path.to_owned(), e))?;
+		serde_json::from_slice(&data).map_err(Error::Json)
+	}
+}
+
+#[derive(Debug)]
+pub enum Error {
+	Io(std::path::PathBuf, std::io::Error),
+	Json(serde_json::Error),
+}
+impl std::fmt::Display for Error {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		match self {
+			Error::Io(path, e) => write!(f, "{}: {e}", path.display()),
+			Error::Json(e) => write!(f, "{e}"),
+		}
+	}
+}
+impl std::error::Error for Error {}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	fn host(s: &str) -> Hostname {
+		Hostname::from_str(s).expect("test host")
+	}
+	fn sd(s: &str) -> SubDir {
+		SubDir::from_str(s).expect("test subdir")
+	}
+
+	#[test]
+	fn parse_basic() {
+		let input = r#"{
+			"version": 1,
+			"dependencies": [
+				{
+					"source": {
+						"git": {
+							"remote": "https://github.com/grafana/jsonnet-libs.git",
+							"subdir": "grafana-builder"
+						}
+					},
+					"version": "54865853ebc1f901964e25a2e7a0e4d2cb6b9648",
+					"sum": "ELsYwK+kGdzX1mee2Yy+/b2mdO4Y503BOCDkFzwmGbE="
+				}
+			],
+			"legacyImports": false
+		}"#;
+
+		let jf: JsonnetFile = serde_json::from_str(input).unwrap();
+		assert_eq!(jf.version, 1);
+		assert!(!jf.legacy_imports);
+		assert_eq!(jf.dependencies.len(), 1);
+
+		let dep = &jf.dependencies[0];
+		let Source::Git(git) = &dep.source else {
+			panic!("expected git source");
+		};
+		assert_eq!(git.host, "github.com");
+		assert_eq!(git.repo, "grafana/jsonnet-libs.git");
+		assert_eq!(git.subdir, "grafana-builder");
+		assert_eq!(
+			git.name(),
+			"github.com/grafana/jsonnet-libs/grafana-builder"
+		);
+		assert_eq!(git.legacy_name(), "grafana-builder");
+		assert_eq!(git.remote(), "https://github.com/grafana/jsonnet-libs.git");
+		assert_eq!(
+			dep.version.as_deref(),
+			Some("54865853ebc1f901964e25a2e7a0e4d2cb6b9648")
+		);
+	}
+
+	#[test]
+	fn parse_local_source() {
+		let input = r#"{
+			"version": 1,
+			"dependencies": [
+				{
+					"source": {
+						"local": { "directory": "../shared-lib" }
+					},
+					"version": ""
+				}
+			]
+		}"#;
+
+		let jf: JsonnetFile = serde_json::from_str(input).unwrap();
+		let dep = &jf.dependencies[0];
+		let Source::Local(local) = &dep.source else {
+			panic!("expected local source");
+		};
+		assert_eq!(local.ups, 1);
+		assert_eq!(local.dir, "shared-lib");
+		assert_eq!(local.to_string(), "../shared-lib");
+		assert!(jf.legacy_imports);
+	}
+
+	#[test]
+	fn parse_uri_github_slug() {
+		let dep = GitSource::parse("github.com/ksonnet/ksonnet-lib/ksonnet.beta.3").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.scheme, GitScheme::Https);
+		assert_eq!(gs.host, "github.com");
+		assert_eq!(gs.repo, "ksonnet/ksonnet-lib");
+		assert_eq!(gs.subdir, "ksonnet.beta.3");
+		assert_eq!(dep.version, None);
+		assert_eq!(gs.remote(), "https://github.com/ksonnet/ksonnet-lib");
+	}
+
+	#[test]
+	fn parse_uri_ssh() {
+		let dep = GitSource::parse("ssh://git@example.com/user/repo.git/foobar@v1").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.scheme, GitScheme::Ssh);
+		assert_eq!(gs.host, "example.com");
+		assert_eq!(gs.repo, "user/repo.git");
+		assert_eq!(gs.subdir, "foobar");
+		assert_eq!(dep.version.as_deref(), Some("v1"));
+		assert_eq!(gs.remote(), "ssh://git@example.com/user/repo.git");
+	}
+
+	#[test]
+	fn parse_uri_scp() {
+		let dep = GitSource::parse("git@my.host:user/repo.git/foobar@v1").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.scheme, GitScheme::Ssh);
+		assert_eq!(gs.host, "my.host");
+		assert_eq!(gs.subdir, "foobar");
+		assert_eq!(dep.version.as_deref(), Some("v1"));
+		assert_eq!(gs.remote(), "ssh://git@my.host/user/repo.git");
+	}
+
+	#[test]
+	fn parse_uri_https_explicit() {
+		let dep = GitSource::parse("https://example.com/foo/bar").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.scheme, GitScheme::Https);
+		assert_eq!(gs.host, "example.com");
+		assert_eq!(gs.repo, "foo/bar");
+		assert_eq!(gs.subdir, "");
+		assert_eq!(gs.remote(), "https://example.com/foo/bar");
+	}
+
+	#[test]
+	fn parse_uri_no_scheme() {
+		let dep = GitSource::parse("example.com/foo/bar").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.scheme, GitScheme::Https);
+		assert_eq!(gs.host, "example.com");
+		assert_eq!(gs.remote(), "https://example.com/foo/bar");
+	}
+
+	#[test]
+	fn parse_uri_path_and_version() {
+		let dep = GitSource::parse("example.com/foo/bar/baz@bat").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.repo, "foo/bar");
+		assert_eq!(gs.subdir, "baz");
+		assert_eq!(dep.version.as_deref(), Some("bat"));
+	}
+
+	#[test]
+	fn parse_uri_version_only() {
+		let dep = GitSource::parse("example.com/foo/bar@baz").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.repo, "foo/bar");
+		assert_eq!(gs.subdir, "");
+		assert_eq!(dep.version.as_deref(), Some("baz"));
+	}
+
+	#[test]
+	fn parse_uri_deep_path() {
+		let dep = GitSource::parse("example.com/foo/bar/baz/bat").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.repo, "foo/bar");
+		assert_eq!(gs.subdir, "baz/bat");
+	}
+
+	#[test]
+	fn parse_uri_subgroups() {
+		let dep = GitSource::parse("example.com/group/subgroup/repository.git").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.repo, "group/subgroup/repository.git");
+		assert_eq!(gs.plain_repo_name(), "group/subgroup/repository");
+		assert_eq!(gs.subdir, "");
+		assert_eq!(
+			gs.remote(),
+			"https://example.com/group/subgroup/repository.git"
+		);
+	}
+
+	#[test]
+	fn parse_uri_subgroup_subdir() {
+		let dep = GitSource::parse("example.com/group/subgroup/repository.git/subdir").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.plain_repo_name(), "group/subgroup/repository");
+		assert_eq!(gs.subdir, "subdir");
+	}
+
+	#[test]
+	fn parse_uri_bitbucket_personal() {
+		let dep = GitSource::parse("bitbucket.org/~user/repository.git").unwrap();
+		let Source::Git(gs) = &dep.source else {
+			panic!()
+		};
+		assert_eq!(gs.host, "bitbucket.org");
+		assert_eq!(gs.repo, "~user/repository.git");
+		assert_eq!(gs.remote(), "https://bitbucket.org/~user/repository.git");
+	}
+
+	#[test]
+	fn name_with_subdir() {
+		let gs = GitSource {
+			scheme: GitScheme::Https,
+			host: host("github.com"),
+			repo: sd("ksonnet/ksonnet-lib"),
+			subdir: sd("ksonnet.beta.3"),
+		};
+		assert_eq!(gs.name(), "github.com/ksonnet/ksonnet-lib/ksonnet.beta.3");
+		assert_eq!(gs.legacy_name(), "ksonnet.beta.3");
+	}
+
+	#[test]
+	fn name_without_subdir() {
+		let gs = GitSource {
+			scheme: GitScheme::Https,
+			host: host("github.com"),
+			repo: sd("user/repo"),
+			subdir: SubDir::empty(),
+		};
+		assert_eq!(gs.name(), "github.com/user/repo");
+		assert_eq!(gs.legacy_name(), "repo");
+	}
+
+	#[test]
+	fn defaults() {
+		let input = r#"{ "version": 1 }"#;
+		let jf: JsonnetFile = serde_json::from_str(input).unwrap();
+		assert!(jf.dependencies.is_empty());
+		assert!(jf.legacy_imports);
+	}
+
+	#[test]
+	fn roundtrip() {
+		let jf = JsonnetFile {
+			version: 1,
+			dependencies: vec![Dependency {
+				source: Source::Git(GitSource {
+					scheme: GitScheme::Https,
+					host: host("github.com"),
+					repo: sd("user/repo"),
+					subdir: sd("lib"),
+				}),
+				version: Some("main".into()),
+				sum: None,
+				name: None,
+				single: false,
+			}],
+			legacy_imports: false,
+		};
+		let json = serde_json::to_string_pretty(&jf).unwrap();
+		let parsed: JsonnetFile = serde_json::from_str(&json).unwrap();
+		assert_eq!(parsed.dependencies.len(), 1);
+		let Source::Git(gs) = &parsed.dependencies[0].source else {
+			panic!()
+		};
+		assert_eq!(gs.host, "github.com");
+		assert_eq!(gs.repo, "user/repo");
+		assert_eq!(gs.subdir, "lib");
+	}
+
+	#[test]
+	fn hostname_rejects_slash() {
+		assert!(Hostname::from_str("foo/bar").is_err());
+		assert!(Hostname::from_str("").is_err());
+		assert!(Hostname::from_str(".").is_err());
+		assert!(Hostname::from_str("..").is_err());
+		assert!(Hostname::from_str(".foo").is_err());
+		assert!(Hostname::from_str("foo.").is_err());
+		assert!(Hostname::from_str("foo..bar").is_err());
+		assert!(Hostname::from_str("foo bar").is_err());
+		assert!(Hostname::from_str("foo.bar").is_ok());
+	}
+
+	#[test]
+	fn subdir_rejects_escape() {
+		assert!(SubDir::from_str("../foo").is_err());
+		assert!(SubDir::from_str("/foo").is_err());
+		assert!(SubDir::from_str("foo/../bar").is_err());
+		assert!(SubDir::from_str("foo/bar").is_ok());
+		assert!(SubDir::from_str("").is_ok());
+	}
+
+	#[test]
+	fn local_source_parse() {
+		let l = LocalSource::from_str("../shared-lib").unwrap();
+		assert_eq!(l.ups, 1);
+		assert_eq!(l.dir, "shared-lib");
+
+		let l = LocalSource::from_str("../../foo/bar").unwrap();
+		assert_eq!(l.ups, 2);
+		assert_eq!(l.dir, "foo/bar");
+
+		let l = LocalSource::from_str("./foo").unwrap();
+		assert_eq!(l.ups, 0);
+		assert_eq!(l.dir, "foo");
+
+		let l = LocalSource::from_str(".").unwrap();
+		assert_eq!(l.ups, 0);
+		assert!(l.dir.is_empty());
+
+		let l = LocalSource::from_str("..").unwrap();
+		assert_eq!(l.ups, 1);
+		assert!(l.dir.is_empty());
+
+		// Mid-path `..` is rejected.
+		assert!(LocalSource::from_str("foo/../bar").is_err());
+		// Absolute path is rejected.
+		assert!(LocalSource::from_str("/foo").is_err());
+	}
+
+	#[test]
+	fn local_source_render_roundtrip() {
+		for s in ["../shared-lib", "../../foo/bar", "foo", "."] {
+			assert_eq!(LocalSource::from_str(s).unwrap().to_string(), s);
+		}
+	}
+
+	#[test]
+	fn local_source_resolve_under() {
+		// `../foo` from `pkg/sub` lands at `pkg/foo`.
+		let l = LocalSource::from_str("../foo").unwrap();
+		assert_eq!(l.resolve_under(&sd("pkg/sub")).unwrap(), "pkg/foo");
+
+		// Plain `foo` from `pkg/sub` lands at `pkg/sub/foo`.
+		let l = LocalSource::from_str("foo").unwrap();
+		assert_eq!(l.resolve_under(&sd("pkg/sub")).unwrap(), "pkg/sub/foo");
+
+		// Too many `..` escapes the parent.
+		let l = LocalSource::from_str("../../../foo").unwrap();
+		assert!(l.resolve_under(&sd("pkg")).is_err());
+	}
+}
addedcrates/jrsonnet-pkg/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-pkg/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod install;
+pub mod jsonnet_bundler;