From da8e25a95c506d2c164cbb1e93f188aaed07f829 Mon Sep 17 00:00:00 2001 From: Prom3theu5 Date: Tue, 2 Jun 2026 01:00:19 +0100 Subject: [PATCH] feat(share): push/pull the indexed graph via an OCI registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Committing the multi-MB graph to git is poor (needs LFS, bloats history). This adds `synapse push` / `synapse pull` so teams share a prebuilt graph through any OCI registry (GHCR/ECR/ACR/Harbor) — teammates pull instead of re-indexing. The graph is shipped as a single-layer OCI artifact (raw .lbug bytes + a small JSON config blob), with git commit / branch / synapse version / blake3 stamped into manifest annotations so identity is verifiable and staleness detectable without downloading the blob. Isolation: all `oci-client` / `tokio` / `docker_credential` usage lives in the new `src/share.rs`; the rest of the crate stays synchronous (the module exposes sync facades that run a short-lived current-thread runtime internally). Gated by a default-on `share` Cargo feature, so `--no-default-features` drops the networking/TLS stack and the push/pull commands. Auth: credentials are auto-discovered from the existing `docker login` (`~/.docker/config.json` + OS credential helpers, incl. macOS keychain) via the `docker_credential` crate — no tokens in synapse's config. Resolution order (auto): env override (SYNAPSE_REGISTRY_USER/PASS/TOKEN) -> docker creds -> anonymous. Public registries pull with zero setup. Push is triple-locked so a fresh clone / CI can't push by accident: `push_enabled = true` in config (default false) AND a clean working tree (or --allow-dirty) AND interactive type-to-confirm (or --yes; non-TTY without --yes refuses rather than hangs). Artifacts are tagged by commit (a per-commit short SHA tag plus the moving `latest`). Pull defaults to the moving tag (`latest`) — NOT the local HEAD commit tag, which usually wouldn't exist for a teammate on a different commit and would hard -fail. It verifies the blob's blake3, writes the graph atomically (temp+rename), records a `.synapse/graph/origin.json` provenance sidecar, and warns loudly when the graph's indexed commit differs from local HEAD. `--tag ` pulls the exact graph for a commit. `status` surfaces the pulled graph's origin commit + staleness on every run (human + --json `origin`/`originStale`); `index` removes the now-stale provenance sidecar. `init` now appends the graph dir to an existing root `.gitignore` (idempotent; leaves `synapse.toml` committable, and never creates a `.gitignore` where none existed) so the binary graph isn't committed. Verified end-to-end against a real Azure Container Registry: push (auth auto-discovered from the macOS keychain, no config secrets) -> pull into a fresh clone -> byte-identical graph, queryable, with the staleness warning firing on a differing HEAD. Full suite (91 tests), fmt and clippy green; the lean `--no-default-features` build still compiles. Bumps version to 0.2.0. New: src/share.rs. Touched: config (ShareConfig), cli (Push/Pull), main (cmd_push/cmd_pull, status origin, index sidecar cleanup, init gitignore), git (full_commit), errors (share variants), Cargo.toml (share feature + deps), README/LLM.md. Tests: share-helper units, push/pull guard CLI tests, gitignore test, ladybug link_edges already covered. --- Cargo.lock | 1894 ++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 12 +- LLM.md | 11 + README.md | 26 + src/cli.rs | 38 + src/config.rs | 56 ++ src/errors.rs | 35 + src/git.rs | 6 + src/lib.rs | 2 + src/main.rs | 294 ++++++++ src/share.rs | 489 +++++++++++++ tests/cli.rs | 157 ++++ tests/unit.rs | 6 + 13 files changed, 2928 insertions(+), 98 deletions(-) create mode 100644 src/share.rs diff --git a/Cargo.lock b/Cargo.lock index d1866ba..9852a5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -88,12 +88,53 @@ 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "blake3" version = "1.8.5" @@ -108,6 +149,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bstr" version = "1.12.1" @@ -124,6 +174,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.63" @@ -131,6 +187,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -140,6 +198,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -219,6 +283,16 @@ 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" @@ -228,7 +302,34 @@ dependencies = [ "encode_unicode", "libc", "unicode-width 0.2.2", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] @@ -237,6 +338,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[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" @@ -277,6 +388,15 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cxx" version = "1.0.138" @@ -336,6 +456,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.5.8" @@ -345,6 +500,76 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "docker_credential" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" +dependencies = [ + "base64", + "serde", + "serde_json", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -363,18 +588,71 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[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", +] + [[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-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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" @@ -388,11 +666,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[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" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "globset" version = "0.4.18" @@ -419,94 +740,420 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "http" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ - "cc", + "bytes", + "itoa", ] [[package]] -name = "ignore" -version = "0.4.25" +name = "http-auth" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" dependencies = [ - "crossbeam-deque", - "globset", - "log", "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "equivalent", - "hashbrown", + "bytes", + "http", ] [[package]] -name = "indicatif" -version = "0.18.4" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "console", - "portable-atomic", - "unicode-width 0.2.2", - "unit-prefix", - "web-time", + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.18" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "js-sys" -version = "0.3.99" +name = "hybrid-array" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", + "typenum", ] [[package]] -name = "lazy_static" +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "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 = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + +[[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" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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 = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "10.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +dependencies = [ + "aws-lc-rs", + "base64", + "getrandom 0.2.17", + "js-sys", + "serde", + "serde_json", + "signature", + "zeroize", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" @@ -542,12 +1189,24 @@ dependencies = [ "cc", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "log" version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +[[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" @@ -563,13 +1222,24 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -579,69 +1249,276 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] -name = "num-traits" -version = "0.2.19" +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oci-client" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5261a7fb43d9c53b8e63e6d5e86860719dad253d015d022066c72d585125aed8" +dependencies = [ + "bytes", + "chrono", + "futures-util", + "hex", + "http", + "http-auth", + "jsonwebtoken", + "lazy_static", + "oci-spec", + "olpc-cjson", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror", + "tokio", + "tracing", + "unicase", +] + +[[package]] +name = "oci-spec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8445a2631507cec628a15fdd6154b54a3ab3f20ed4fe9d73a3b8b7a4e1ba03a" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "autocfg", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "quinn-proto" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] [[package]] -name = "portable-atomic" -version = "1.13.1" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "powerfmt" -version = "0.2.0" +name = "rand" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "unicode-ident", + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] -name = "quick-xml" -version = "0.40.1" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "memchr", + "getrandom 0.2.17", ] [[package]] -name = "quote" -version = "1.0.45" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "proc-macro2", + "getrandom 0.3.4", ] [[package]] @@ -673,6 +1550,61 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "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 0.9.0", + "windows-sys 0.52.0", +] + [[package]] name = "rust_decimal" version = "1.42.0" @@ -683,12 +1615,108 @@ dependencies = [ "num-traits", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +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 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -698,12 +1726,50 @@ dependencies = [ "winapi-util", ] +[[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 = "scratch" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" +[[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" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -757,6 +1823,29 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -772,6 +1861,31 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[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 = "slab" version = "0.4.12" @@ -784,6 +1898,22 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +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" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "streaming-iterator" version = "0.1.9" @@ -796,6 +1926,30 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[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" @@ -809,20 +1963,23 @@ dependencies = [ [[package]] name = "synapse" -version = "0.1.5" +version = "0.2.0" dependencies = [ "anyhow", "blake3", "chrono", "clap", + "docker_credential", "globset", "ignore", "indicatif", "lbug", + "oci-client", "quick-xml", "serde", "serde_json", "thiserror", + "tokio", "toml", "tracing", "tracing-subscriber", @@ -840,6 +1997,26 @@ dependencies = [ "tree-sitter-yaml", ] +[[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" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -892,10 +2069,84 @@ dependencies = [ ] [[package]] -name = "time-core" -version = "0.1.8" +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] [[package]] name = "toml" @@ -936,12 +2187,58 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +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 = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1127,12 +2424,39 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" 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.1.14" @@ -1145,12 +2469,48 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[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" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1183,6 +2543,30 @@ dependencies = [ "winapi-util", ] +[[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" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -1196,6 +2580,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.122" @@ -1228,6 +2622,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -1238,13 +2655,22 @@ dependencies = [ "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-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1306,6 +2732,24 @@ dependencies = [ "windows-link", ] +[[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" @@ -1315,12 +2759,270 @@ dependencies = [ "windows-link", ] +[[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 = "winnow" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index e029a70..73f37ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "synapse" -version = "0.1.5" +version = "0.2.0" edition = "2024" [[bin]] @@ -8,11 +8,19 @@ name = "synapse" path = "src/main.rs" [features] -default = ["ladybug"] +default = ["ladybug", "share"] ladybug = ["dep:lbug"] +# Share the indexed graph via an OCI registry (`synapse push` / `pull`). +# Pulls in an async (tokio) + TLS stack, so it's a toggle for lean builds. +share = ["dep:oci-client", "dep:tokio", "dep:docker_credential"] [dependencies] lbug = { version = "0.17.0", optional = true } +oci-client = { version = "0.17", default-features = false, features = [ + "rustls-tls", +], optional = true } +tokio = { version = "1", features = ["rt"], optional = true } +docker_credential = { version = "1.3", optional = true } anyhow = "1.0.102" blake3 = "1.8.5" chrono = { version = "0.4.44", features = ["serde"] } diff --git a/LLM.md b/LLM.md index a17570d..66ef7f9 100644 --- a/LLM.md +++ b/LLM.md @@ -86,6 +86,17 @@ synapse explore # Ladybug Explorer UI on http://localhost:8000 (read- synapse explore --print # just print the docker command (no Docker needed) ``` +## Share the graph via an OCI registry (optional) +```bash +synapse pull # fetch the team's shared graph (tag "latest") instead of indexing +synapse pull --tag # fetch the graph for a specific commit +synapse push --yes # publish (restricted; CI skips the confirm prompt) +``` +- Configured in the `[share]` section of synapse.toml (`registry`, `repository`, `push_enabled`). +- Auth is auto-discovered from `docker login` (no tokens in config); anonymous for public registries; env `SYNAPSE_REGISTRY_USER`/`PASS`/`TOKEN` override for headless. +- `pull` verifies integrity and WARNS if the graph's commit != local HEAD (it may be stale → `synapse index`). `status` shows the pulled `Origin:` commit + `originStale` in `--json`. +- Push is gated: needs `push_enabled=true` + clean tree (or `--allow-dirty`) + confirm (or `--yes`). A fresh clone can't push by accident. + ## Gotchas - Re-run `synapse index` after code changes; stale results otherwise (`status` shows `staleFiles`). - `pack` needs exactly one of `--changed/--path/--symbol/--query`. diff --git a/README.md b/README.md index b8579af..f979183 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ synapse pack --symbol MyHandler --output context.md synapse pack --changed --budget 40000 --output context.md --explain synapse pack --symbol MyHandler --format json # structured output for tools synapse explore # visualize the graph in a browser +synapse pull # fetch a shared graph from an OCI registry +synapse push --yes # publish the graph (restricted; see below) synapse clean --all ``` @@ -85,8 +87,32 @@ try `--tag dev` or a matching image tag. | `packages` | List indexed package dependencies (or `--projects`), with versions resolved via .NET Central Package Management (`--ecosystem`, `--json`). Use `--importers ` for impact analysis: the files that import a package. | | `explore` | Launch [Ladybug Explorer](https://docs.ladybugdb.com/visualization/) (Docker) to visualize the indexed graph in a browser (`--port`, `--read-write`, `--detach`, `--in-memory`, `--tag`, `--print`). Mounts the index read-only by default; `--print` shows the `docker run` command without executing it. | | `pack` | Emit a context pack (`--changed`/`--path`/`--symbol`/`--query`, `--budget`, `--include-tests`, `--include-config`, `--include-diff`, `--dry-run`, `--explain`, `--output`). `--format markdown` (default) or `--format json` for programmatic callers. Writes to stdout unless `--output` is given; diagnostics go to stderr. | +| `pull` | Fetch a shared graph from the configured OCI registry (`--tag`, `--registry`, `--repository`). Verifies integrity (blake3), writes the graph atomically, and warns if its indexed commit differs from local `HEAD`. | +| `push` | Publish the indexed graph to the configured OCI registry (`--tag`, `--registry`, `--repository`, `--yes`, `--allow-dirty`). Restricted — see [Sharing the graph](#sharing-the-graph-oci-registry). | | `clean` | Remove `--cache` / `--index` / `--packs` / `--all`. | +## Sharing the graph (OCI registry) + +The indexed graph (`synapse.lbug`) is a multi-MB binary — too heavy for git. Instead, share it with your team via any OCI registry (GHCR, ECR, ACR, Harbor, …): CI (or a maintainer) `push`es the graph, teammates `pull` it instead of re-indexing. + +```bash +# One-time: point the [share] section at your registry (in .synapse/synapse.toml) +# [share] +# registry = "ghcr.io" +# repository = "myorg/myrepo-synapse-graph" +# push_enabled = true # required to allow `push` at all (default false) + +synapse pull # fetch the current shared graph (tag "latest") +synapse pull --tag a1b2c3d # fetch the graph for a specific commit +synapse push --yes # publish (CI-friendly; skips the confirm prompt) +``` + +- **Credentials are auto-discovered** from your existing `docker login` (`~/.docker/config.json` + OS credential helpers). You do **not** put a token in synapse's config. Public registries pull anonymously with no setup. For headless setups without a docker config, set `SYNAPSE_REGISTRY_USER` + `SYNAPSE_REGISTRY_PASS` (or `SYNAPSE_REGISTRY_TOKEN`). +- **Push is heavily guarded** (a fresh clone / CI can never push by accident): it requires `push_enabled = true` in config **and** a clean working tree (or `--allow-dirty`) **and** interactive type-to-confirm (or `--yes`). The graph is tagged by commit (a per-commit tag plus the moving `latest`), so a tag always describes a known state. +- **Staleness is surfaced, never hidden.** `pull` warns loudly when the graph's indexed commit differs from your `HEAD`, and `synapse status` shows the pulled graph's `Origin:` commit on every run. If it's stale, `synapse index` rebuilds locally. +- **Visibility = the registry's visibility.** The graph encodes file paths, symbol names and dependencies — treat a public registry accordingly. +- Sharing is the `share` Cargo feature (on by default); a `--no-default-features` build without it drops the networking/TLS stack and the `push`/`pull` commands. + ## Languages Symbol extraction (via tree-sitter) covers **C#, Rust, Python, Go, JavaScript, diff --git a/src/cli.rs b/src/cli.rs index 2f1a8f5..07c8ac6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,10 +29,48 @@ pub enum Command { Pack(PackArgs), /// Launch Ladybug Explorer (Docker) to visualize the indexed graph. Explore(ExploreArgs), + /// Push the indexed graph to the configured OCI registry (restricted). + Push(PushArgs), + /// Pull an indexed graph from the configured OCI registry. + Pull(PullArgs), /// Remove cached/index/pack data. Clean(CleanArgs), } +#[derive(Debug, clap::Args)] +pub struct PushArgs { + /// Override the per-commit tag (defaults to the short HEAD commit SHA). + #[arg(long)] + pub tag: Option, + /// Override the configured registry host for this invocation. + #[arg(long)] + pub registry: Option, + /// Override the configured repository for this invocation. + #[arg(long)] + pub repository: Option, + /// Skip the interactive type-to-confirm prompt (for CI). Does NOT bypass + /// `push_enabled` or the dirty-tree guard. + #[arg(long)] + pub yes: bool, + /// Allow pushing from a working tree with uncommitted changes. + #[arg(long)] + pub allow_dirty: bool, +} + +#[derive(Debug, clap::Args)] +pub struct PullArgs { + /// Tag to pull (defaults to the HEAD commit tag if present, else the + /// configured moving tag). + #[arg(long)] + pub tag: Option, + /// Override the configured registry host for this invocation. + #[arg(long)] + pub registry: Option, + /// Override the configured repository for this invocation. + #[arg(long)] + pub repository: Option, +} + #[derive(Debug, clap::Args)] pub struct InitArgs { /// Overwrite an existing config. diff --git a/src/config.rs b/src/config.rs index f108741..498f71a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,8 @@ pub struct SynapseConfig { pub index: IndexConfig, pub graph: GraphConfig, pub pack: PackConfig, + #[serde(default)] + pub share: ShareConfig, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -80,6 +82,59 @@ pub struct PackConfig { pub include_selection_reasons: bool, } +/// Sharing the indexed graph via an OCI registry (`synapse push` / `pull`). +/// +/// Push is OFF by default (`push_enabled = false`) — it must be explicitly +/// enabled, and even then requires interactive confirmation and a clean tree. +/// Credentials are never stored here: they are discovered from the existing +/// docker login (`~/.docker/config.json` + credential helpers). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ShareConfig { + /// Registry host[:port], e.g. "ghcr.io". Empty = sharing not configured. + #[serde(default)] + pub registry: String, + /// Repository path within the registry, e.g. "myorg/myrepo-synapse-graph". + /// Empty = sharing not configured. + #[serde(default)] + pub repository: String, + /// Moving tag updated on every push alongside the per-commit tag. + #[serde(default = "default_share_moving_tag")] + pub moving_tag: String, + /// MUST be true for `synapse push` to be permitted at all. + #[serde(default)] + pub push_enabled: bool, + /// Transport: "https" (default) or "http" (plaintext; dev/local only). + #[serde(default = "default_share_protocol")] + pub protocol: String, + /// Credential strategy: "auto" (docker creds -> env -> anonymous), + /// "docker", "env", or "anonymous". + #[serde(default = "default_share_auth")] + pub auth: String, +} + +impl Default for ShareConfig { + fn default() -> Self { + ShareConfig { + registry: String::new(), + repository: String::new(), + moving_tag: default_share_moving_tag(), + push_enabled: false, + protocol: default_share_protocol(), + auth: default_share_auth(), + } + } +} + +fn default_share_moving_tag() -> String { + "latest".to_string() +} +fn default_share_protocol() -> String { + "https".to_string() +} +fn default_share_auth() -> String { + "auto".to_string() +} + fn default_root() -> String { ".".to_string() } @@ -132,6 +187,7 @@ impl Default for SynapseConfig { default_format: default_format(), include_selection_reasons: true, }, + share: ShareConfig::default(), } } } diff --git a/src/errors.rs b/src/errors.rs index c6855aa..47594c5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -27,4 +27,39 @@ pub enum SynapseError { /// The graph backend reported a failure. #[error("graph backend error: {0}")] Backend(String), + + /// `synapse push` was invoked but `[share].push_enabled` is false. + #[error( + "push is disabled; set `push_enabled = true` in the [share] section of .synapse/synapse.toml to allow it" + )] + PushDisabled, + + /// The registry/repository for sharing is not configured. + #[error( + "share target not configured; set `registry` and `repository` in the [share] section (or pass --registry/--repository)" + )] + ShareNotConfigured, + + /// Push refused because the working tree has uncommitted changes. + #[error( + "working tree has uncommitted changes ({0} file(s)); commit/stash or pass --allow-dirty (the graph is tagged by commit)" + )] + DirtyTree(usize), + + /// Push was not confirmed (interactive prompt declined or non-interactive + /// without `--yes`). + #[error("push not confirmed (interactive confirmation required, or pass --yes)")] + PushNotConfirmed, + + /// A registry network/transport call failed. + #[error("registry network error: {0}")] + RegistryNetwork(String), + + /// Registry authentication failed. + #[error("registry authentication failed: {0}")] + RegistryAuth(String), + + /// A pulled graph failed its blake3 integrity check. + #[error("pulled graph failed integrity check (blake3 mismatch); the artifact may be corrupt")] + IntegrityMismatch, } diff --git a/src/git.rs b/src/git.rs index 7486e3b..05f975f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -35,6 +35,12 @@ pub fn info(root: &Path) -> GitInfo { } } +/// The full (40-char) commit SHA of HEAD, or `None` outside a git repo / with no +/// commits. Used as the canonical identity for a shared graph artifact. +pub fn full_commit(root: &Path) -> Option { + run(root, &["rev-parse", "HEAD"]).filter(|s| !s.is_empty()) +} + /// True if `root` is inside a git work tree. pub fn is_git_repo(root: &Path) -> bool { run(root, &["rev-parse", "--is-inside-work-tree"]) diff --git a/src/lib.rs b/src/lib.rs index 9e1f849..5fb7b8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,3 +15,5 @@ pub mod indexer; pub mod output; pub mod pack; pub mod repo; +#[cfg(feature = "share")] +pub mod share; diff --git a/src/main.rs b/src/main.rs index d6633ca..93802b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,14 @@ fn run() -> Result<()> { Command::Packages(args) => cmd_packages(&cwd, args), Command::Pack(args) => cmd_pack(&cwd, args), Command::Explore(args) => cmd_explore(&cwd, args), + #[cfg(feature = "share")] + Command::Push(args) => cmd_push(&cwd, args), + #[cfg(feature = "share")] + Command::Pull(args) => cmd_pull(&cwd, args), + #[cfg(not(feature = "share"))] + Command::Push(_) | Command::Pull(_) => anyhow::bail!( + "this build was compiled without the `share` feature; rebuild with `--features share` to push/pull graphs" + ), Command::Clean(args) => cmd_clean(&cwd, args), } } @@ -98,16 +106,72 @@ fn cmd_init(cwd: &Path, args: cli::InitArgs) -> Result<()> { } config.save(&repo.root)?; + // If the repo has a root .gitignore, make sure the local graph working state + // is ignored — the graph is a multi-MB binary that should be shared via + // `synapse push`, never committed. We deliberately keep `synapse.toml` + // committable (it's shared team config), so we ignore the working subdirs, + // not the whole `.synapse/`. + if let Some(added) = ensure_gitignored(&repo.root, &config)? { + println!("Added {added} to .gitignore"); + } + println!("Initialized Synapse workspace at {}", dir.display()); println!(" config: {}", cfg_path.display()); println!(" graph: {}", dir.join("graph").display()); Ok(()) } +/// If a root `.gitignore` exists, ensure the synapse graph working state is +/// ignored. Returns the entry added, or `None` if nothing changed (no +/// `.gitignore`, or already covered). Only the graph/cache/packs working dirs +/// are ignored — `synapse.toml` stays committable as shared config. +fn ensure_gitignored(root: &Path, config: &SynapseConfig) -> Result> { + let gitignore = root.join(".gitignore"); + if !gitignore.is_file() { + // Respect a repo that deliberately has no .gitignore. + return Ok(None); + } + let text = std::fs::read_to_string(&gitignore) + .with_context(|| format!("reading {}", gitignore.display()))?; + + // Derive the graph dir relative to the repo root (handles a custom + // `graph.path`); fall back to the default working subdirs of `.synapse`. + let graph_rel = config.graph.path.trim_end_matches('/'); + let entry = format!("{graph_rel}/"); + + // Already covered? Accept the exact entry, or a broader ignore of the + // synapse dir / a bare graph glob. + let already = text.lines().map(str::trim).any(|l| { + l == entry + || l == graph_rel + || l == ".synapse/" + || l == ".synapse" + || l == "**/synapse.lbug" + }); + if already { + return Ok(None); + } + + let mut new_text = text; + if !new_text.is_empty() && !new_text.ends_with('\n') { + new_text.push('\n'); + } + new_text.push_str("\n# SimCube Synapse local graph (share via `synapse push`, don't commit)\n"); + new_text.push_str(&entry); + new_text.push('\n'); + std::fs::write(&gitignore, new_text) + .with_context(|| format!("updating {}", gitignore.display()))?; + Ok(Some(entry)) +} + fn cmd_index(cwd: &Path, args: cli::IndexArgs) -> Result<()> { let (repo, config) = require_repo(cwd)?; let store = open_store(&repo.root, &config)?; + // Re-indexing makes the graph locally-derived, so any registry-pull + // provenance marker no longer applies — remove it (best effort). + let _ = std::fs::remove_file(graph_origin_path(&repo.root, &config)); + // Count candidates for the summary (walked-and-eligible files). let candidates = repo.candidate_files(&config)?; let now = now_rfc3339(); @@ -239,6 +303,18 @@ fn cmd_status(cwd: &Path, args: cli::StatusArgs) -> Result<()> { .unwrap_or_default(); let ready = !indexed.is_empty(); + // Provenance: if this graph was pulled from a registry, surface its origin + // commit and whether it matches local HEAD (staleness, even days later). + let origin = read_graph_origin(&repo.root, &config); + let origin_commit = origin + .as_ref() + .and_then(|o| o.get("commit").and_then(|c| c.as_str())) + .map(|s| s.to_string()); + let origin_stale = match (&origin_commit, git::full_commit(&repo.root)) { + (Some(g), Some(h)) => Some(!g.starts_with(&h) && !h.starts_with(g.as_str())), + _ => None, + }; + if args.stale { for s in &stale { println!("{s}"); @@ -265,6 +341,8 @@ fn cmd_status(cwd: &Path, args: cli::StatusArgs) -> Result<()> { "branch": info.branch, "commit": info.commit, "lastIndex": last_index, + "origin": origin, + "originStale": origin_stale, }); println!("{}", serde_json::to_string_pretty(&obj)?); return Ok(()); @@ -287,9 +365,33 @@ fn cmd_status(cwd: &Path, args: cli::StatusArgs) -> Result<()> { if !last_index.is_empty() { println!("Last index: {last_index}"); } + if let Some(o) = &origin { + let reg = o.get("registry").and_then(|v| v.as_str()).unwrap_or("?"); + let repo_s = o.get("repository").and_then(|v| v.as_str()).unwrap_or("?"); + let tag = o.get("tag").and_then(|v| v.as_str()).unwrap_or("?"); + let commit = origin_commit.as_deref().unwrap_or("?"); + println!("Origin: {reg}/{repo_s}:{tag} (commit {commit})"); + if origin_stale == Some(true) { + // Loud staleness note to stderr (data stays on stdout). + eprintln!( + "warning: pulled graph was indexed at commit {commit}, but local HEAD differs; run `synapse index` to refresh" + ); + } + } Ok(()) } +/// Path to the graph provenance sidecar written by `synapse pull`. +fn graph_origin_path(root: &Path, config: &SynapseConfig) -> std::path::PathBuf { + root.join(&config.graph.path).join("origin.json") +} + +/// Read the provenance sidecar (`origin.json`) if present; `None` otherwise. +fn read_graph_origin(root: &Path, config: &SynapseConfig) -> Option { + let text = std::fs::read_to_string(graph_origin_path(root, config)).ok()?; + serde_json::from_str(&text).ok() +} + fn cmd_symbols(cwd: &Path, args: cli::SymbolsArgs) -> Result<()> { let (repo, config) = require_repo(cwd)?; let store = open_store(&repo.root, &config)?; @@ -590,6 +692,198 @@ fn cmd_explore(cwd: &Path, args: cli::ExploreArgs) -> Result<()> { Ok(()) } +/// Resolve registry + repository from config with per-invocation overrides. +/// Errors if neither config nor override supplies both. +#[cfg(feature = "share")] +fn resolve_share_coords( + config: &SynapseConfig, + registry_override: Option, + repository_override: Option, +) -> Result<(String, String)> { + let registry = registry_override + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| config.share.registry.clone()); + let repository = repository_override + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| config.share.repository.clone()); + if registry.is_empty() || repository.is_empty() { + return Err(errors::SynapseError::ShareNotConfigured.into()); + } + Ok((registry, repository)) +} + +#[cfg(feature = "share")] +fn cmd_pull(cwd: &Path, args: cli::PullArgs) -> Result<()> { + use synapse::share; + + let (repo, config) = require_repo(cwd)?; + let (registry, repository) = + resolve_share_coords(&config, args.registry.clone(), args.repository.clone())?; + + let tag = share::resolve_pull_tag(args.tag.as_deref(), &config.share.moving_tag); + let target = share::ShareTarget { + registry, + repository, + tag, + }; + + eprintln!("Pulling graph: {}", target.display()); + let pulled = share::pull_graph(&config.share, &target)?; + + // Atomic write: temp file + rename, so an interrupted pull never leaves a + // half-written graph in place. + let graph_dir = repo.root.join(&config.graph.path); + std::fs::create_dir_all(&graph_dir) + .with_context(|| format!("creating {}", graph_dir.display()))?; + let final_path = graph_dir.join("synapse.lbug"); + if final_path.exists() { + eprintln!( + "warning: overwriting existing local graph at {}", + final_path.display() + ); + } + let tmp_path = graph_dir.join("synapse.lbug.tmp"); + std::fs::write(&tmp_path, &pulled.bytes) + .with_context(|| format!("writing {}", tmp_path.display()))?; + std::fs::rename(&tmp_path, &final_path) + .with_context(|| format!("replacing {}", final_path.display()))?; + + // Provenance sidecar. + let origin = serde_json::json!({ + "source": "registry", + "registry": target.registry, + "repository": target.repository, + "tag": target.tag, + "commit": pulled.meta.commit, + "branch": pulled.meta.branch, + "synapseVersion": pulled.meta.synapse_version, + "blake3": pulled.meta.blob_blake3, + "pulledAt": now_rfc3339(), + }); + let _ = std::fs::write( + graph_origin_path(&repo.root, &config), + serde_json::to_string_pretty(&origin).unwrap_or_default(), + ); + + // Staleness: warn loudly if the graph's commit differs from local HEAD. + let head_full = git::full_commit(&repo.root); + match share::compare_commit(pulled.meta.commit.as_deref(), head_full.as_deref()) { + share::GraphFreshness::Mismatch { + graph_commit, + head_commit, + } => { + eprintln!( + "warning: pulled graph was indexed at commit {graph_commit}, but local HEAD is {head_commit}; \ + symbols/edges may not reflect your working tree — run `synapse index` to refresh" + ); + } + share::GraphFreshness::Match => { + eprintln!("Graph matches local HEAD commit."); + } + share::GraphFreshness::Unknown => {} + } + + println!( + "Pulled {} bytes to {} (tag {})", + fmt_num(pulled.bytes.len()), + final_path.display(), + target.tag, + ); + Ok(()) +} + +#[cfg(feature = "share")] +fn cmd_push(cwd: &Path, args: cli::PushArgs) -> Result<()> { + use synapse::share; + + let (repo, config) = require_repo(cwd)?; + + // Guard 1: push must be explicitly enabled in config. + if !config.share.push_enabled { + return Err(errors::SynapseError::PushDisabled.into()); + } + // Guard 2: registry + repository configured. + let (registry, repository) = + resolve_share_coords(&config, args.registry.clone(), args.repository.clone())?; + + // Guard 3: the graph must exist. + let graph_path = repo.root.join(&config.graph.path).join("synapse.lbug"); + let bytes = std::fs::read(&graph_path).map_err(|_| { + anyhow::anyhow!( + "no graph at {} — run `synapse index` first", + graph_path.display() + ) + })?; + + // Guard 4: clean working tree (unless --allow-dirty), so the commit tag + // actually describes what's in the graph. + if !args.allow_dirty { + let changed = git::changed_files(&repo.root); + if !changed.is_empty() { + return Err(errors::SynapseError::DirtyTree(changed.len()).into()); + } + } + + // Guard 5: a real commit to tag by. + let full_commit = git::full_commit(&repo.root).ok_or_else(|| { + anyhow::anyhow!("cannot determine HEAD commit; not a git repo with commits") + })?; + let short_commit = git::info(&repo.root).commit; + let tags = share::push_tags( + args.tag.as_deref(), + short_commit.as_deref(), + &config.share.moving_tag, + ); + + // Guard 6: interactive type-to-confirm (unless --yes). Refuse on a + // non-interactive stdin rather than hang. + if !args.yes { + if !std::io::stdin().is_terminal() { + return Err(errors::SynapseError::PushNotConfirmed.into()); + } + eprintln!("About to push the graph to:"); + for t in &tags { + eprintln!(" {registry}/{repository}:{t}"); + } + eprint!("Type the repository ({repository}) to confirm: "); + use std::io::Write; + std::io::stderr().flush().ok(); + let mut line = String::new(); + std::io::stdin() + .read_line(&mut line) + .context("reading confirmation")?; + if line.trim() != repository { + return Err(errors::SynapseError::PushNotConfirmed.into()); + } + } + + let info = git::info(&repo.root); + let meta = share::GraphArtifactMeta { + commit: Some(full_commit), + branch: info.branch, + repo_name: if config.repo.name.is_empty() { + None + } else { + Some(config.repo.name.clone()) + }, + synapse_version: Some(env!("CARGO_PKG_VERSION").to_string()), + blob_blake3: Some(blake3::hash(&bytes).to_hex().to_string()), + created_at: Some(now_rfc3339()), + }; + + eprintln!("Pushing graph ({} bytes)…", fmt_num(bytes.len())); + let outcome = share::push_graph(&config.share, ®istry, &repository, &tags, bytes, &meta)?; + + println!( + "Pushed {} to {}/{} (tags: {})", + outcome.digest, + registry, + repository, + outcome.references.join(", "), + ); + Ok(()) +} + fn cmd_clean(cwd: &Path, args: cli::CleanArgs) -> Result<()> { let (repo, _config) = require_repo(cwd)?; let dir = config::synapse_dir(&repo.root); diff --git a/src/share.rs b/src/share.rs new file mode 100644 index 0000000..2299f00 --- /dev/null +++ b/src/share.rs @@ -0,0 +1,489 @@ +//! Share the indexed graph via an OCI registry (`synapse push` / `pull`). +//! +//! This is the ONLY module that depends on `oci-client`, `tokio` and +//! `docker_credential`; everything else in the crate stays synchronous. The +//! public surface is sync — network methods spin up a short-lived current-thread +//! tokio runtime internally (see `block_on`) so callers never touch async. +//! +//! The graph is shipped as a single-layer OCI artifact: the raw `synapse.lbug` +//! bytes as one layer, a tiny JSON config blob, and the git/version/blake3 +//! metadata stamped into manifest annotations so identity is verifiable and +//! staleness detectable without downloading the (multi-MB) blob. + +use crate::config::ShareConfig; +use crate::errors::SynapseError; +use anyhow::{Result, anyhow}; +use oci_client::client::{ClientConfig, ClientProtocol, ImageLayer}; +use oci_client::manifest::OciImageManifest; +use oci_client::secrets::RegistryAuth; +use oci_client::{Client, Reference, client::Config as OciConfig}; +use std::collections::BTreeMap; +use std::future::Future; + +// --- artifact media types --------------------------------------------------- + +/// Media type of the graph layer (the raw `.lbug` bytes). +pub const GRAPH_LAYER_MEDIA_TYPE: &str = "application/vnd.simcube.synapse.graph.v1+lbug"; +/// Media type of the small JSON config blob. +pub const GRAPH_CONFIG_MEDIA_TYPE: &str = "application/vnd.simcube.synapse.graph.config.v1+json"; + +// --- annotation keys --------------------------------------------------------- + +/// Full commit SHA the graph was indexed at (OCI standard key). +pub const ANNOT_REVISION: &str = "org.opencontainers.image.revision"; +/// RFC3339 creation timestamp (OCI standard key). +pub const ANNOT_CREATED: &str = "org.opencontainers.image.created"; +/// synapse version that produced the graph (OCI standard key). +pub const ANNOT_VERSION: &str = "org.opencontainers.image.version"; +/// Repo name / artifact title (OCI standard key). +pub const ANNOT_TITLE: &str = "org.opencontainers.image.title"; +/// Branch the graph was indexed on (synapse namespace). +pub const ANNOT_BRANCH: &str = "com.simcube.synapse.branch"; +/// blake3 of the graph blob (synapse namespace). +pub const ANNOT_BLAKE3: &str = "com.simcube.synapse.blake3"; +/// Full commit SHA, duplicated in our namespace (robust to tooling that drops +/// the standard `revision` key). +pub const ANNOT_COMMIT: &str = "com.simcube.synapse.commit"; + +/// Metadata describing a shared graph artifact — carried in manifest +/// annotations (and mirrored into the config blob). All fields optional so a +/// partially-annotated artifact still parses. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct GraphArtifactMeta { + /// Full commit SHA the graph was indexed at. + pub commit: Option, + pub branch: Option, + pub repo_name: Option, + pub synapse_version: Option, + /// blake3 hex of the graph blob, for integrity verification on pull. + pub blob_blake3: Option, + pub created_at: Option, +} + +impl GraphArtifactMeta { + /// Render to the OCI manifest annotation map (omitting empty fields). + pub fn to_annotations(&self) -> BTreeMap { + let mut a = BTreeMap::new(); + let mut put = |k: &str, v: &Option| { + if let Some(val) = v.as_ref().filter(|s| !s.is_empty()) { + a.insert(k.to_string(), val.clone()); + } + }; + put(ANNOT_REVISION, &self.commit); + put(ANNOT_COMMIT, &self.commit); + put(ANNOT_CREATED, &self.created_at); + put(ANNOT_VERSION, &self.synapse_version); + put(ANNOT_TITLE, &self.repo_name); + put(ANNOT_BRANCH, &self.branch); + put(ANNOT_BLAKE3, &self.blob_blake3); + a + } + + /// Parse from an OCI manifest annotation map. Prefers our namespaced commit + /// key, falling back to the OCI standard `revision`. + pub fn from_annotations(a: &BTreeMap) -> Self { + let get = |k: &str| a.get(k).filter(|s| !s.is_empty()).cloned(); + GraphArtifactMeta { + commit: get(ANNOT_COMMIT).or_else(|| get(ANNOT_REVISION)), + branch: get(ANNOT_BRANCH), + repo_name: get(ANNOT_TITLE), + synapse_version: get(ANNOT_VERSION), + blob_blake3: get(ANNOT_BLAKE3), + created_at: get(ANNOT_CREATED), + } + } + + /// The self-describing JSON config blob bytes for this artifact. + pub fn to_config_blob(&self) -> Vec { + let obj = serde_json::json!({ + "schemaVersion": 1, + "tool": "synapse", + "synapseVersion": self.synapse_version, + "commit": self.commit, + "branch": self.branch, + "repo": self.repo_name, + "blake3": self.blob_blake3, + "createdAt": self.created_at, + }); + serde_json::to_vec(&obj).unwrap_or_else(|_| b"{}".to_vec()) + } +} + +// --- staleness --------------------------------------------------------------- + +/// Result of comparing a shared graph's indexed commit against local HEAD. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GraphFreshness { + /// The graph was indexed at the same commit as local HEAD. + Match, + /// The graph's commit differs from local HEAD — results may be stale. + Mismatch { + graph_commit: String, + head_commit: String, + }, + /// Can't tell (not a git repo, or the artifact has no commit annotation). + Unknown, +} + +/// Compare a graph's indexed commit against local HEAD. Prefix-tolerant so a +/// short SHA on either side still matches a full SHA (e.g. a per-commit tag vs +/// the full annotation). `Unknown` when either side is absent. +pub fn compare_commit(graph_commit: Option<&str>, head_commit: Option<&str>) -> GraphFreshness { + match (graph_commit, head_commit) { + (Some(g), Some(h)) if !g.is_empty() && !h.is_empty() => { + let n = g.len().min(h.len()); + if g[..n].eq_ignore_ascii_case(&h[..n]) { + GraphFreshness::Match + } else { + GraphFreshness::Mismatch { + graph_commit: g.to_string(), + head_commit: h.to_string(), + } + } + } + _ => GraphFreshness::Unknown, + } +} + +// --- tag resolution ---------------------------------------------------------- + +/// Resolve which tag a `pull` should fetch: +/// 1. an explicit `--tag` override (e.g. a specific commit SHA); +/// 2. otherwise the configured moving tag (e.g. "latest"). +/// +/// The default deliberately does NOT auto-pick the local HEAD commit tag: a +/// teammate is usually on a *different* commit than the pushed graph, so that +/// tag typically wouldn't exist in the registry and the pull would hard-fail +/// with "manifest unknown". Pulling the moving tag and then warning on commit +/// mismatch (see `compare_commit`) is the robust, predictable behaviour — fetch +/// the current shared graph, then surface staleness. Use `--tag ` to pull +/// the exact graph for a specific commit. +pub fn resolve_pull_tag(explicit: Option<&str>, moving_tag: &str) -> String { + explicit + .filter(|s| !s.is_empty()) + .map(|t| t.to_string()) + .unwrap_or_else(|| moving_tag.to_string()) +} + +/// The two tags a `push` writes: the immutable per-commit tag (short SHA) and +/// the moving tag. An explicit `--tag` overrides the per-commit tag. +pub fn push_tags( + explicit: Option<&str>, + head_short: Option<&str>, + moving_tag: &str, +) -> Vec { + let mut tags = Vec::new(); + match explicit.filter(|s| !s.is_empty()) { + Some(t) => tags.push(t.to_string()), + None => { + if let Some(sha) = head_short.filter(|s| !s.is_empty()) { + tags.push(sha.to_string()); + } + } + } + if !moving_tag.is_empty() && !tags.iter().any(|t| t == moving_tag) { + tags.push(moving_tag.to_string()); + } + tags +} + +// --- network layer (the only async / oci-client surface) -------------------- + +/// Resolved registry coordinates for a single push/pull. +#[derive(Debug, Clone)] +pub struct ShareTarget { + pub registry: String, + pub repository: String, + pub tag: String, +} + +impl ShareTarget { + fn reference(&self) -> Reference { + Reference::with_tag( + self.registry.clone(), + self.repository.clone(), + self.tag.clone(), + ) + } + + /// `registry/repository:tag` for display. + pub fn display(&self) -> String { + format!("{}/{}:{}", self.registry, self.repository, self.tag) + } +} + +/// A pulled graph: the raw blob bytes plus the parsed artifact metadata. +pub struct PulledGraph { + pub bytes: Vec, + pub meta: GraphArtifactMeta, +} + +/// Result of a successful push. +pub struct PushOutcome { + /// The references (tags) the artifact was pushed under. + pub references: Vec, + pub digest: String, +} + +/// Run a future to completion on a short-lived current-thread tokio runtime. +/// Keeps all async confined to this module so the rest of the CLI stays sync. +fn block_on(fut: F) -> Result { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| anyhow!("starting async runtime: {e}"))?; + Ok(rt.block_on(fut)) +} + +fn build_client(cfg: &ShareConfig) -> Client { + let protocol = match cfg.protocol.as_str() { + "http" => ClientProtocol::Http, + _ => ClientProtocol::Https, + }; + Client::new(ClientConfig { + protocol, + ..Default::default() + }) +} + +/// Resolve registry credentials. Order for `auto`: env override -> docker +/// credentials (`~/.docker/config.json` + helpers) -> anonymous. Credentials +/// are never read from synapse's own config. +fn resolve_auth(cfg: &ShareConfig, registry: &str) -> RegistryAuth { + let from_env = || -> Option { + if let Ok(token) = std::env::var("SYNAPSE_REGISTRY_TOKEN") + && !token.is_empty() + { + return Some(RegistryAuth::Bearer(token)); + } + match ( + std::env::var("SYNAPSE_REGISTRY_USER"), + std::env::var("SYNAPSE_REGISTRY_PASS"), + ) { + (Ok(u), Ok(p)) if !u.is_empty() => Some(RegistryAuth::Basic(u, p)), + _ => None, + } + }; + let from_docker = || -> Option { + match docker_credential::get_credential(registry) { + Ok(docker_credential::DockerCredential::UsernamePassword(u, p)) => { + Some(RegistryAuth::Basic(u, p)) + } + Ok(docker_credential::DockerCredential::IdentityToken(t)) => { + Some(RegistryAuth::Bearer(t)) + } + Err(_) => None, + } + }; + + let resolved = match cfg.auth.as_str() { + "anonymous" => None, + "env" => from_env(), + "docker" => from_docker(), + // "auto" (default): env override first, then docker, then anonymous. + _ => from_env().or_else(from_docker), + }; + resolved.unwrap_or(RegistryAuth::Anonymous) +} + +/// Map an oci-client error to a synapse domain error, distinguishing auth +/// failures from generic network errors. Credentials are never included. +fn map_oci_err(e: oci_client::errors::OciDistributionError) -> SynapseError { + let msg = e.to_string(); + let lower = msg.to_ascii_lowercase(); + if lower.contains("auth") || lower.contains("401") || lower.contains("unauthorized") { + SynapseError::RegistryAuth(msg) + } else { + SynapseError::RegistryNetwork(msg) + } +} + +/// Read just the manifest annotations for `target` — cheap, no blob download. +/// Used for the pre-pull staleness check and `status`. +pub fn fetch_meta(cfg: &ShareConfig, target: &ShareTarget) -> Result { + let client = build_client(cfg); + let auth = resolve_auth(cfg, &target.registry); + let reference = target.reference(); + let (manifest, _digest) = + block_on(async { client.pull_image_manifest(&reference, &auth).await })? + .map_err(map_oci_err)?; + Ok(meta_from_manifest(&manifest)) +} + +fn meta_from_manifest(manifest: &OciImageManifest) -> GraphArtifactMeta { + manifest + .annotations + .as_ref() + .map(GraphArtifactMeta::from_annotations) + .unwrap_or_default() +} + +/// Pull the graph blob + metadata, verifying the blob's blake3 against the +/// artifact annotation (hard error on mismatch — corruption/tampering). +pub fn pull_graph(cfg: &ShareConfig, target: &ShareTarget) -> Result { + let client = build_client(cfg); + let auth = resolve_auth(cfg, &target.registry); + let reference = target.reference(); + + let image = block_on(async { + client + .pull(&reference, &auth, vec![GRAPH_LAYER_MEDIA_TYPE]) + .await + })? + .map_err(map_oci_err)?; + + let meta = image + .manifest + .as_ref() + .map(meta_from_manifest) + .unwrap_or_default(); + + let layer = image + .layers + .into_iter() + .next() + .ok_or_else(|| anyhow!("pulled artifact has no layers"))?; + let bytes = layer.data.to_vec(); + + // Integrity: recompute blake3 and compare to the annotation when present. + if let Some(expected) = meta.blob_blake3.as_ref() { + let actual = blake3::hash(&bytes).to_hex().to_string(); + if &actual != expected { + return Err(SynapseError::IntegrityMismatch.into()); + } + } + + Ok(PulledGraph { bytes, meta }) +} + +/// Push the graph blob as a single-layer artifact under each tag, stamping +/// `meta` into the manifest annotations and config blob. The caller has already +/// run all push guards (see `cmd_push`); this is pure transport. +pub fn push_graph( + cfg: &ShareConfig, + registry: &str, + repository: &str, + tags: &[String], + bytes: Vec, + meta: &GraphArtifactMeta, +) -> Result { + let client = build_client(cfg); + let auth = resolve_auth(cfg, registry); + + let layers = vec![ImageLayer::new( + bytes, + GRAPH_LAYER_MEDIA_TYPE.to_string(), + None, + )]; + let config = OciConfig::new( + meta.to_config_blob(), + GRAPH_CONFIG_MEDIA_TYPE.to_string(), + None, + ); + let annotations = meta.to_annotations(); + + let mut digest = String::new(); + for tag in tags { + let reference = + Reference::with_tag(registry.to_string(), repository.to_string(), tag.clone()); + let mut manifest = OciImageManifest::build(&layers, &config, Some(annotations.clone())); + manifest.artifact_type = Some(GRAPH_LAYER_MEDIA_TYPE.to_string()); + let resp = block_on(async { + client + .push(&reference, &layers, config.clone(), &auth, Some(manifest)) + .await + })? + .map_err(map_oci_err)?; + digest = resp.manifest_url; + } + + Ok(PushOutcome { + references: tags.to_vec(), + digest, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compare_commit_matches_equal_and_prefixes() { + assert_eq!( + compare_commit(Some("abcdef123456"), Some("abcdef123456")), + GraphFreshness::Match + ); + // short tag vs full annotation: prefix match. + assert_eq!( + compare_commit(Some("abcdef1"), Some("abcdef123456")), + GraphFreshness::Match + ); + assert!(matches!( + compare_commit(Some("abcdef1"), Some("999999123456")), + GraphFreshness::Mismatch { .. } + )); + assert_eq!(compare_commit(None, Some("abc")), GraphFreshness::Unknown); + assert_eq!(compare_commit(Some("abc"), None), GraphFreshness::Unknown); + } + + #[test] + fn tag_resolution_precedence() { + // explicit wins. + assert_eq!(resolve_pull_tag(Some("v1"), "latest"), "v1"); + // otherwise the moving tag (NOT the local HEAD commit — that usually + // wouldn't exist in the registry for a teammate on a different commit). + assert_eq!(resolve_pull_tag(None, "latest"), "latest"); + assert_eq!(resolve_pull_tag(Some(""), "latest"), "latest"); + } + + #[test] + fn push_tags_include_commit_and_moving() { + let tags = push_tags(None, Some("abc1234"), "latest"); + assert_eq!(tags, vec!["abc1234".to_string(), "latest".to_string()]); + // explicit replaces the per-commit tag but moving tag still added. + let tags = push_tags(Some("release"), Some("abc1234"), "latest"); + assert_eq!(tags, vec!["release".to_string(), "latest".to_string()]); + // no duplicate when explicit == moving tag. + let tags = push_tags(Some("latest"), Some("abc1234"), "latest"); + assert_eq!(tags, vec!["latest".to_string()]); + } + + #[test] + fn annotations_roundtrip() { + let meta = GraphArtifactMeta { + commit: Some("abcdef123456".into()), + branch: Some("main".into()), + repo_name: Some("synapse".into()), + synapse_version: Some("0.1.5".into()), + blob_blake3: Some("deadbeef".into()), + created_at: Some("2026-06-02T00:00:00+00:00".into()), + }; + let a = meta.to_annotations(); + // Standard + namespaced commit keys both present. + assert_eq!(a.get(ANNOT_REVISION).unwrap(), "abcdef123456"); + assert_eq!(a.get(ANNOT_COMMIT).unwrap(), "abcdef123456"); + assert_eq!(a.get(ANNOT_BLAKE3).unwrap(), "deadbeef"); + // Round-trips back to the same metadata. + assert_eq!(GraphArtifactMeta::from_annotations(&a), meta); + } + + #[test] + fn empty_fields_are_omitted_from_annotations() { + let meta = GraphArtifactMeta { + commit: Some("abc".into()), + ..Default::default() + }; + let a = meta.to_annotations(); + assert!(a.contains_key(ANNOT_COMMIT)); + assert!(!a.contains_key(ANNOT_BRANCH)); + assert!(!a.contains_key(ANNOT_BLAKE3)); + } + + #[test] + fn share_config_default_is_pull_only() { + let c = crate::config::ShareConfig::default(); + assert!(!c.push_enabled); + assert_eq!(c.protocol, "https"); + assert_eq!(c.auth, "auto"); + } +} diff --git a/tests/cli.rs b/tests/cli.rs index ddb81af..d825128 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -538,3 +538,160 @@ fn test_explore_print_command() { let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir2); } + +// --- share (push/pull) guard tests ------------------------------------------ +// These verify the safety gates with NO network: a fresh/default workspace must +// never push, and misconfiguration must fail loudly. They assume the binary was +// built with the default `share` feature. + +/// A git repo with one committed file, so HEAD resolves to a real commit (the +/// share tests need a commit to tag the graph by). `git_init` alone makes an +/// empty repo with no HEAD when there are no fixtures to commit. +fn git_repo_with_commit(name: &str) -> PathBuf { + let dir = make_temp_dir(name); + std::fs::write(dir.join("README.md"), "# test\n").unwrap(); + git_init(&dir); + dir +} + +/// Enable push + set a (fake) registry/repository in an initialized workspace. +fn enable_push_config(dir: &Path) { + let cfg = dir.join(".synapse/synapse.toml"); + let text = std::fs::read_to_string(&cfg) + .unwrap() + .replace("push_enabled = false", "push_enabled = true") + .replace("registry = \"\"", "registry = \"localhost:5000\"") + .replace("repository = \"\"", "repository = \"team/graph\""); + std::fs::write(&cfg, text).unwrap(); +} + +/// `synapse push` on a default workspace must refuse: push is disabled. +#[test] +fn test_push_disabled_by_default() { + let dir = make_temp_dir("push_disabled"); + git_init(&dir); + run_ok(&dir, &["init", "--name", "p"]); + let out = run(&dir, &["push", "--yes"]); + assert!(!out.status.success(), "push must fail when disabled"); + assert!( + stderr(&out).contains("push is disabled"), + "stderr: {}", + stderr(&out) + ); +} + +/// With push enabled but no registry/repository, push must report the +/// misconfiguration rather than attempting any network call. +#[test] +fn test_push_enabled_but_unconfigured() { + let dir = make_temp_dir("push_unconfigured"); + git_init(&dir); + run_ok(&dir, &["init", "--name", "p"]); + // Enable push in the config (registry/repository left empty). + let cfg = dir.join(".synapse/synapse.toml"); + let mut text = std::fs::read_to_string(&cfg).unwrap(); + text = text.replace("push_enabled = false", "push_enabled = true"); + std::fs::write(&cfg, text).unwrap(); + + let out = run(&dir, &["push", "--yes"]); + assert!(!out.status.success(), "push must fail when unconfigured"); + assert!( + stderr(&out).contains("share target not configured"), + "stderr: {}", + stderr(&out) + ); +} + +/// Push from a dirty working tree must refuse without --allow-dirty. +#[test] +fn test_push_refuses_dirty_tree() { + let dir = git_repo_with_commit("push_dirty"); + run_ok(&dir, &["init", "--name", "p"]); + enable_push_config(&dir); + // Make the tree dirty with a new file, and index so the graph exists. + std::fs::write(dir.join("dirty.txt"), "uncommitted").unwrap(); + run_ok(&dir, &["index"]); + + let out = run(&dir, &["push", "--yes"]); + assert!(!out.status.success(), "push must fail on a dirty tree"); + assert!( + stderr(&out).contains("uncommitted changes"), + "stderr: {}", + stderr(&out) + ); +} + +/// Non-interactive push without --yes must refuse rather than hang (the test +/// harness has no TTY on stdin). +#[test] +fn test_push_non_interactive_without_yes() { + let dir = git_repo_with_commit("push_no_tty"); + run_ok(&dir, &["init", "--name", "p"]); + enable_push_config(&dir); + run_ok(&dir, &["index"]); + + // No --yes, stdin is not a TTY → must refuse (not block). Pass + // --allow-dirty so we reach the confirmation guard (the untracked + // .synapse/ dir would otherwise trip the dirty-tree guard first). + let out = run(&dir, &["push", "--allow-dirty"]); + assert!( + !out.status.success(), + "push must refuse without confirmation" + ); + assert!( + stderr(&out).contains("not confirmed"), + "stderr: {}", + stderr(&out) + ); +} + +/// `synapse pull` with no configured registry must report misconfiguration. +#[test] +fn test_pull_unconfigured() { + let dir = git_repo_with_commit("pull_unconfigured"); + run_ok(&dir, &["init", "--name", "p"]); + let out = run(&dir, &["pull"]); + assert!(!out.status.success(), "pull must fail when unconfigured"); + assert!( + stderr(&out).contains("share target not configured"), + "stderr: {}", + stderr(&out) + ); +} + +/// `synapse init` adds the graph dir to an existing root .gitignore (so the +/// multi-MB graph isn't committed), but leaves a repo without one untouched. +#[test] +fn test_init_gitignores_graph_dir() { + // With an existing .gitignore: the graph dir gets appended. + let dir = git_repo_with_commit("init_gitignore"); + std::fs::write(dir.join(".gitignore"), "target/\n").unwrap(); + run_ok(&dir, &["init", "--name", "p"]); + let gi = std::fs::read_to_string(dir.join(".gitignore")).unwrap(); + assert!( + gi.contains(".synapse/graph/"), + "graph dir should be ignored: {gi}" + ); + // synapse.toml stays committable (not ignored). + assert!( + !gi.contains("synapse.toml"), + "config must stay committable: {gi}" + ); + + // Idempotent: a second init doesn't duplicate the entry. + run_ok(&dir, &["init", "--force", "--name", "p"]); + let gi2 = std::fs::read_to_string(dir.join(".gitignore")).unwrap(); + assert_eq!( + gi2.matches(".synapse/graph/").count(), + 1, + "entry must not be duplicated: {gi2}" + ); + + // No .gitignore present → init does not create one. + let dir2 = git_repo_with_commit("init_no_gitignore"); + run_ok(&dir2, &["init", "--name", "p"]); + assert!( + !dir2.join(".gitignore").exists(), + "init must not create a .gitignore where none existed" + ); +} diff --git a/tests/unit.rs b/tests/unit.rs index 05308b3..378f348 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -40,6 +40,12 @@ fn config_default_roundtrips_through_toml() { let toml = cfg.to_toml().expect("serialize"); assert!(toml.contains("backend = \"ladybug\"")); assert!(toml.contains("default_budget = 40000")); + // The [share] section persists and is pull-only by default. + assert!(toml.contains("[share]"), "share section present: {toml}"); + assert!( + toml.contains("push_enabled = false"), + "push disabled by default: {toml}" + ); let parsed: SynapseConfig = toml::from_str(&toml).expect("parse"); assert_eq!(parsed, cfg); }