diff --git a/Cargo.lock b/Cargo.lock index a221eb7..9b72649 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -44,6 +59,17 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a902604543851c9ccc59d6252eb77502a9e01d6bf7205dd2ab4b543bd22cbda3" +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -80,7 +106,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]] @@ -91,7 +117,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -100,6 +126,12 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "assert_cmd" version = "2.2.2" @@ -116,14 +148,34 @@ dependencies = [ ] [[package]] -name = "atomic" -version = "0.6.1" +name = "async-compression" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ - "bytemuck", + "proc-macro2", + "quote", + "syn 3.0.3", ] +[[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" @@ -136,6 +188,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -164,7 +222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f0f0ef446ddbb042aac62a2da0b680e116b6cb2b1f79c20eda2a9107611408" dependencies = [ "aneubeck-daachorse", - "base64", + "base64 0.22.1", "fnv", "itertools", "serde", @@ -176,7 +234,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b25deec0a72cf5d8b66a6e487c5ee9757b0639f3e16d687f6c6dea834812000c" dependencies = [ - "base64", + "base64 0.22.1", "bpe", "either", "flate2", @@ -186,6 +244,27 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.13.1" @@ -210,10 +289,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] -name = "bytemuck" -version = "1.25.2" +name = "bytes" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "camino" @@ -224,6 +303,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -298,8 +387,8 @@ dependencies = [ "camino", "clap", "colophon-core", - "directories", "indicatif", + "librebar", "owo-colors", "predicates", "serde", @@ -317,9 +406,8 @@ dependencies = [ "ah-ah-ah", "anyhow", "camino", - "directories", - "figment", "indicatif", + "librebar", "pulldown-cmark", "regex", "serde", @@ -329,8 +417,6 @@ dependencies = [ "tempfile", "thiserror", "tracing", - "tracing-appender", - "tracing-subscriber", "typst-syntax", "walkdir", "which", @@ -343,6 +429,24 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + [[package]] name = "console" version = "0.16.4" @@ -352,7 +456,7 @@ dependencies = [ "encode_unicode", "libc", "unicode-width", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -428,7 +532,18 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -452,6 +567,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -465,7 +598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -486,20 +619,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "figment" -version = "0.10.19" +name = "filetime" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ - "atomic", - "serde", - "serde_json", - "serde_yaml", - "toml", - "uncased", - "version_check", + "cfg-if", + "libc", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + [[package]] name = "flate2" version = "1.1.9" @@ -531,12 +665,36 @@ 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 = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.34" @@ -586,6 +744,35 @@ dependencies = [ "r-efi", ] +[[package]] +name = "granit-parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c92814c286f45b5ed1498ce9547bc9637b473eba7037f8aee6a4d5a3c1e051c" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -627,6 +814,205 @@ dependencies = [ "serde_json", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "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 = "indexmap" version = "2.14.0" @@ -658,7 +1044,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -705,6 +1091,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leakguard" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6f14c2513f98281a34f17c7c184cff041a7dca9cd5ee8bf55b9e4712081f56" + [[package]] name = "levenshtein" version = "1.0.5" @@ -723,6 +1115,46 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "librebar" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10be87448f91f4710ddd3ca3573e382ab7c2a7a1c1cfbe1f154c982be6bdf5ae" +dependencies = [ + "async-trait", + "base64 0.23.1", + "bytes", + "camino", + "clap", + "clap_complete", + "clap_mangen", + "directories", + "http", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "leakguard", + "owo-colors", + "rust-zstd", + "rustls", + "semver", + "serde", + "serde-saphyr", + "serde_json", + "serde_path_to_error", + "tar", + "tempfile", + "thiserror", + "tokio", + "toml 1.1.4+spec-1.1.0", + "tower", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + [[package]] name = "libredox" version = "0.1.19" @@ -738,6 +1170,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" version = "0.4.14" @@ -784,6 +1222,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -796,7 +1245,7 @@ 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]] @@ -882,6 +1331,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[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" @@ -894,6 +1349,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +[[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" @@ -1043,10 +1507,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] -name = "rmp" -version = "0.8.15" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ "num-traits", ] @@ -1067,6 +1545,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" +[[package]] +name = "rust-zstd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d556913dfad2fe0a10328b1a99c3bb834acbe26fd267bd3b3b3f3032d7533804" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1083,7 +1567,41 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1141,6 +1659,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2c6921c8ce48fb1b052b457619d008834039d80325992ed3bc55bbe6c155ad" +dependencies = [ + "annotate-snippets", + "encoding_rs_io", + "granit-parser", + "num-traits", + "serde_core", + "smallvec", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -1174,6 +1706,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1183,6 +1726,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1205,6 +1757,22 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -1229,6 +1797,22 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +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 = "stop-words" version = "0.10.0" @@ -1253,6 +1837,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "2.1.0" @@ -1300,6 +1890,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tabled" version = "0.21.0" @@ -1310,6 +1917,16 @@ dependencies = [ "testing_table", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -1320,7 +1937,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1403,6 +2020,16 @@ dependencies = [ "time-core", ] +[[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.12.0" @@ -1418,6 +2045,57 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -1425,11 +2103,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -1439,6 +2132,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -1447,10 +2149,19 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", ] [[package]] @@ -1459,6 +2170,62 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[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", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "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" @@ -1533,6 +2300,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typst-syntax" version = "0.15.1" @@ -1542,7 +2315,7 @@ dependencies = [ "ecow", "rustc-hash", "serde", - "toml", + "toml 0.8.23", "typst-timing", "typst-utils", "unicode-ident", @@ -1581,15 +2354,6 @@ dependencies = [ "unicode-math-class", ] -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - [[package]] name = "unicase" version = "2.9.0" @@ -1653,6 +2417,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" +[[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" @@ -1665,12 +2453,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wait-timeout" version = "0.2.1" @@ -1690,6 +2472,15 @@ 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" @@ -1751,6 +2542,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "8.0.5" @@ -1766,7 +2566,7 @@ 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]] @@ -1775,6 +2575,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1784,6 +2593,70 @@ 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", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.15" @@ -1793,14 +2666,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "xtask" version = "0.1.0" dependencies = [ "clap", "clap_complete", - "clap_mangen", "colophon", + "librebar", ] [[package]] @@ -1818,6 +2703,89 @@ dependencies = [ "streaming-stats", ] +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +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 2.0.119", + "synstructure", +] + +[[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 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[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 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/colophon-core/Cargo.toml b/crates/colophon-core/Cargo.toml index 3a4f718..6575ea4 100644 --- a/crates/colophon-core/Cargo.toml +++ b/crates/colophon-core/Cargo.toml @@ -29,15 +29,12 @@ exclude = [ [dependencies] camino = { version = "1.2", features = ["serde1"] } -figment = { version = "0.10", features = ["toml", "yaml", "json"] } +librebar = { version = "0.6", default-features = false, features = ["config"] } serde = { version = "1.0", features = ["derive"] } -directories = "6.0" thiserror = "2.0" tracing = "0.1" anyhow = "1.0" serde_json = "1.0" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-appender = "0.2" # Markdown parsing pulldown-cmark = "0.13" diff --git a/crates/colophon-core/src/config.rs b/crates/colophon-core/src/config.rs index 37ae9ed..4f0a039 100644 --- a/crates/colophon-core/src/config.rs +++ b/crates/colophon-core/src/config.rs @@ -1,9 +1,11 @@ //! Configuration loading and discovery. //! -//! This module provides configuration file discovery by: +//! Discovery is delegated to [`librebar::config`]; this module supplies +//! colophon's config schema and pins the application name. Sources are: //! 1. Walking up from the current directory to find project config //! 2. Loading user config from XDG config directory -//! 3. Merging with sensible defaults +//! 3. Reading `COLOPHON_`-prefixed environment variables +//! 4. Merging with sensible defaults //! //! # Supported formats //! @@ -34,12 +36,16 @@ //! ``` use camino::{Utf8Path, Utf8PathBuf}; -use figment::Figment; -use figment::providers::{Format, Json, Serialized, Toml, Yaml}; use serde::{Deserialize, Serialize}; use crate::error::{ConfigError, ConfigResult}; +/// Metadata about which configuration sources were loaded. +/// +/// Returned alongside [`Config`] from [`ConfigLoader::load()`] so commands +/// can report the actual config files without re-discovering them. +pub use librebar::config::ConfigSources; + /// Configuration for source file discovery. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(default)] @@ -293,102 +299,81 @@ impl LogLevel { } } -/// Metadata about which configuration sources were loaded. -/// -/// Returned alongside [`Config`] from [`ConfigLoader::load()`] so commands -/// can report the actual config files without re-discovering them. -#[derive(Debug, Clone, Default, Serialize)] -pub struct ConfigSources { - /// Project config file found by walking up from the search root. - #[serde(skip_serializing_if = "Option::is_none")] - pub project_file: Option, - /// User config file from XDG config directory. - #[serde(skip_serializing_if = "Option::is_none")] - pub user_file: Option, - /// Explicit config files loaded (e.g., from `--config` flag). - #[serde(skip_serializing_if = "Vec::is_empty")] - pub explicit_files: Vec, -} +/// Application name for XDG directory lookup and config file names. +const APP_NAME: &str = "colophon"; -impl ConfigSources { - /// Returns the highest-precedence config file that was loaded. - /// - /// Precedence: explicit files > project file > user file. - pub fn primary_file(&self) -> Option<&Utf8Path> { - self.explicit_files - .last() - .map(Utf8PathBuf::as_path) - .or(self.project_file.as_deref()) - .or(self.user_file.as_deref()) +/// Translate a librebar loader error into colophon's config error. +fn map_load_error(error: librebar::Error) -> ConfigError { + match error { + librebar::Error::ConfigNotFound => ConfigError::NotFound, + other => ConfigError::Deserialize(Box::new(other)), } } -/// Supported configuration file extensions (in order of preference). -const CONFIG_EXTENSIONS: &[&str] = &["toml", "yaml", "yml", "json"]; - -/// Application name for XDG directory lookup and config file names. -const APP_NAME: &str = "colophon"; - /// Builder for loading configuration from multiple sources. -#[derive(Debug, Default)] +/// +/// Thin wrapper over [`librebar::config::ConfigLoader`] that pins the +/// application name to `colophon` and the config type to [`Config`]. +#[derive(Debug)] pub struct ConfigLoader { - /// Starting directory for project config search. - project_search_root: Option, - /// Whether to include user config from XDG directory. - include_user_config: bool, - /// Stop searching when we hit a directory containing this file/dir. - boundary_marker: Option, - /// Explicit config files to load (for testing or programmatic use). - explicit_files: Vec, + inner: librebar::config::ConfigLoader, +} + +impl Default for ConfigLoader { + fn default() -> Self { + Self::new() + } } impl ConfigLoader { /// Create a new config loader with default settings. pub fn new() -> Self { Self { - project_search_root: None, - include_user_config: true, - boundary_marker: Some(".git".to_string()), - explicit_files: Vec::new(), + inner: librebar::config::ConfigLoader::new(APP_NAME), } } /// Set the starting directory for project config search. /// /// The loader will walk up from this directory looking for config files. - pub fn with_project_search>(mut self, path: P) -> Self { - self.project_search_root = Some(path.as_ref().to_path_buf()); - self + pub fn with_project_search>(self, path: P) -> Self { + Self { + inner: self.inner.with_project_search(path), + } } /// Set whether to include user config from `~/.config/colophon/`. - pub const fn with_user_config(mut self, include: bool) -> Self { - self.include_user_config = include; - self + pub fn with_user_config(self, include: bool) -> Self { + Self { + inner: self.inner.with_user_config(include), + } } /// Set a boundary marker to stop directory traversal. /// /// When walking up directories, stop if we find a directory containing /// this file or directory name. Default is `.git`. - pub fn with_boundary_marker>(mut self, marker: S) -> Self { - self.boundary_marker = Some(marker.into()); - self + pub fn with_boundary_marker>(self, marker: S) -> Self { + Self { + inner: self.inner.with_boundary_marker(marker), + } } /// Disable boundary marker (search all the way to filesystem root). - pub fn without_boundary_marker(mut self) -> Self { - self.boundary_marker = None; - self + pub fn without_boundary_marker(self) -> Self { + Self { + inner: self.inner.without_boundary_marker(), + } } /// Add an explicit config file to load. /// /// Files are loaded in order, with later files taking precedence. /// Explicit files are loaded after discovered files. - pub fn with_file>(mut self, path: P) -> Self { - self.explicit_files.push(path.as_ref().to_path_buf()); - self + pub fn with_file>(self, path: P) -> Self { + Self { + inner: self.inner.with_file(path), + } } /// Load configuration, merging all discovered sources. @@ -399,137 +384,28 @@ impl ConfigLoader { /// /// Precedence (highest to lowest): /// 1. Explicit files (in order added via `with_file`) - /// 2. Project config (closest to search root) - /// 3. User config (`~/.config/colophon/config.`) - /// 4. Default values - #[tracing::instrument(skip(self), fields(search_root = ?self.project_search_root))] + /// 2. `COLOPHON_`-prefixed environment variables + /// 3. Project config (closest to search root) + /// 4. User config (`~/.config/colophon/config.`) + /// 5. Default values + /// + /// # Errors + /// + /// Returns [`ConfigError::Deserialize`] if a config file cannot be read + /// or parsed, or if the merged result does not match [`Config`]. pub fn load(self) -> ConfigResult<(Config, ConfigSources)> { - tracing::debug!("loading configuration"); - let mut figment = Figment::new().merge(Serialized::defaults(Config::default())); - let mut sources = ConfigSources::default(); - - // Start with user config (lowest precedence of file sources) - if self.include_user_config - && let Some(user_config) = self.find_user_config() - { - figment = Self::merge_file(figment, &user_config); - sources.user_file = Some(user_config); - } - - // Add project config - if let Some(ref root) = self.project_search_root - && let Some(project_config) = self.find_project_config(root) - { - figment = Self::merge_file(figment, &project_config); - sources.project_file = Some(project_config); - } - - // Add explicit files (highest precedence) - for file in &self.explicit_files { - figment = Self::merge_file(figment, file); - } - sources.explicit_files = self.explicit_files; - - let config: Config = figment - .extract() - .map_err(|e| ConfigError::Deserialize(Box::new(e)))?; - tracing::info!( - log_level = config.log_level.as_str(), - "configuration loaded" - ); - Ok((config, sources)) + self.inner.load::().map_err(map_load_error) } - /// Load configuration, returning an error if no config file is found. + /// Load configuration, returning an error if no config source is found. + /// + /// # Errors + /// + /// Returns [`ConfigError::NotFound`] if no config file, environment + /// variable, or override supplied any value. pub fn load_or_error(self) -> ConfigResult<(Config, ConfigSources)> { - let has_user = self.include_user_config && self.find_user_config().is_some(); - let has_project = self - .project_search_root - .as_ref() - .and_then(|root| self.find_project_config(root)) - .is_some(); - let has_explicit = !self.explicit_files.is_empty(); - - if !has_user && !has_project && !has_explicit { - return Err(ConfigError::NotFound); - } - - self.load() + self.inner.load_or_error::().map_err(map_load_error) } - - /// Find project config by walking up from the given directory. - fn find_project_config(&self, start: &Utf8Path) -> Option { - let mut current = Some(start.to_path_buf()); - - while let Some(dir) = current { - // Check for config files in this directory (try each extension) - for ext in CONFIG_EXTENSIONS { - // Try .config/ directory first (.config/colophon.toml) - let dotconfig = dir.join(format!(".config/{APP_NAME}.{ext}")); - if dotconfig.is_file() { - return Some(dotconfig); - } - - // Then dotfile (.colophon.toml) - let dotfile = dir.join(format!(".{APP_NAME}.{ext}")); - if dotfile.is_file() { - return Some(dotfile); - } - - // Then regular name (colophon.toml) - let regular = dir.join(format!("{APP_NAME}.{ext}")); - if regular.is_file() { - return Some(regular); - } - } - - // Check for boundary marker AFTER checking config files, - // so a config in the same directory as the marker is found. - if let Some(ref marker) = self.boundary_marker - && dir.join(marker).exists() - && dir != start - { - break; - } - - current = dir.parent().map(Utf8Path::to_path_buf); - } - - None - } - - /// Find user config in XDG config directory. - fn find_user_config(&self) -> Option { - let proj_dirs = directories::ProjectDirs::from("", "", APP_NAME)?; - let config_dir = proj_dirs.config_dir(); - - // Try each supported extension - for ext in CONFIG_EXTENSIONS { - let config_path = config_dir.join(format!("config.{ext}")); - if config_path.is_file() { - return Utf8PathBuf::from_path_buf(config_path).ok(); - } - } - - None - } - - /// Merge a config file into the figment, detecting format from extension. - fn merge_file(figment: Figment, path: &Utf8Path) -> Figment { - match path.extension() { - Some("toml") => figment.merge(Toml::file_exact(path.as_str())), - Some("yaml" | "yml") => figment.merge(Yaml::file_exact(path.as_str())), - Some("json") => figment.merge(Json::file_exact(path.as_str())), - _ => figment.merge(Toml::file_exact(path.as_str())), - } - } -} - -/// Get the project directories for XDG-compliant path resolution. -/// -/// Returns `None` if the home directory cannot be determined. -fn project_dirs() -> Option { - directories::ProjectDirs::from("", "", APP_NAME) } /// Get the user config directory path. @@ -537,8 +413,7 @@ fn project_dirs() -> Option { /// Returns `~/.config/colophon/` on Linux, `~/Library/Application Support/colophon/` /// on macOS, and equivalent on other platforms. pub fn user_config_dir() -> Option { - let proj_dirs = project_dirs()?; - Utf8PathBuf::from_path_buf(proj_dirs.config_dir().to_path_buf()).ok() + librebar::config::user_config_dir(APP_NAME) } /// Get the user cache directory path. @@ -546,8 +421,7 @@ pub fn user_config_dir() -> Option { /// Returns `~/.cache/colophon/` on Linux, `~/Library/Caches/colophon/` /// on macOS, and equivalent on other platforms. pub fn user_cache_dir() -> Option { - let proj_dirs = project_dirs()?; - Utf8PathBuf::from_path_buf(proj_dirs.cache_dir().to_path_buf()).ok() + librebar::config::user_cache_dir(APP_NAME) } /// Get the user data directory path. @@ -555,8 +429,7 @@ pub fn user_cache_dir() -> Option { /// Returns `~/.local/share/colophon/` on Linux, `~/Library/Application Support/colophon/` /// on macOS, and equivalent on other platforms. pub fn user_data_dir() -> Option { - let proj_dirs = project_dirs()?; - Utf8PathBuf::from_path_buf(proj_dirs.data_dir().to_path_buf()).ok() + librebar::config::user_data_dir(APP_NAME) } /// Get the local data directory path (machine-specific, not synced). @@ -564,8 +437,7 @@ pub fn user_data_dir() -> Option { /// Returns `~/.local/share/colophon/` on Linux, `~/Library/Application Support/colophon/` /// on macOS, and equivalent on other platforms. pub fn user_data_local_dir() -> Option { - let proj_dirs = project_dirs()?; - Utf8PathBuf::from_path_buf(proj_dirs.data_local_dir().to_path_buf()).ok() + librebar::config::user_data_local_dir(APP_NAME) } #[cfg(test)] diff --git a/crates/colophon-core/src/error.rs b/crates/colophon-core/src/error.rs index d396885..a13cea2 100644 --- a/crates/colophon-core/src/error.rs +++ b/crates/colophon-core/src/error.rs @@ -5,9 +5,12 @@ use thiserror::Error; /// Errors that can occur when working with configuration. #[derive(Error, Debug)] pub enum ConfigError { - /// Failed to deserialize configuration. + /// Failed to load or deserialize configuration. #[error("invalid configuration: {0}")] - Deserialize(#[from] Box), + Deserialize( + /// The underlying loader error. + Box, + ), /// Configuration file not found after searching all locations. #[error("no configuration file found")] diff --git a/crates/colophon-core/src/lib.rs b/crates/colophon-core/src/lib.rs index 4d1bb5e..41c817c 100644 --- a/crates/colophon-core/src/lib.rs +++ b/crates/colophon-core/src/lib.rs @@ -34,8 +34,6 @@ pub mod error; pub mod extract; -pub mod observability; - pub mod render; pub(crate) mod typst_prose; diff --git a/crates/colophon-core/src/observability.rs b/crates/colophon-core/src/observability.rs deleted file mode 100644 index 9f9ece1..0000000 --- a/crates/colophon-core/src/observability.rs +++ /dev/null @@ -1,508 +0,0 @@ -//! Observability setup: structured logging. -//! -//! **Important**: This module never writes to stdout, which is reserved for -//! application output (e.g., MCP server communication). All logging goes to -//! files or stderr. -use anyhow::Result; -use serde_json::{Map, Value}; -use std::fs::OpenOptions; -use std::io::Write; -use std::path::{Path, PathBuf}; -use tracing::Event; -use tracing_subscriber::filter::EnvFilter; -use tracing_subscriber::layer::{Context as LayerContext, SubscriberExt}; -use tracing_subscriber::registry::LookupSpan; -use tracing_subscriber::util::SubscriberInitExt; -const ENV_LOG_PATH: &str = "COLOPHON_LOG_PATH"; -const ENV_LOG_DIR: &str = "COLOPHON_LOG_DIR"; -const DEFAULT_LOG_DIR_UNIX: &str = "/var/log"; -const LOG_FILE_SUFFIX: &str = ".jsonl"; - -/// Configuration for observability setup. -#[derive(Clone, Debug)] -pub struct ObservabilityConfig { - /// The service name used in log entries and traces. - pub service: String, - /// Directory for JSONL log files. Falls back to platform defaults if unset. - pub log_dir: Option, -} - -impl ObservabilityConfig { - /// Create config from environment variables with optional overrides. - pub fn from_env_with_overrides(log_dir: Option) -> Self { - Self { - service: env!("CARGO_PKG_NAME").to_string(), - log_dir, - } - } -} - -#[derive(Clone, Debug)] -struct LogTarget { - dir: PathBuf, - file_name: String, -} - -impl LogTarget { - #[cfg(test)] - fn path(&self) -> PathBuf { - self.dir.join(&self.file_name) - } -} - -/// Guard that must be held for the lifetime of the application to ensure -/// proper cleanup of logging and tracing resources. -pub struct ObservabilityGuard { - _log_guard: tracing_appender::non_blocking::WorkerGuard, -} - -/// Initialize observability (logging). -/// -/// Returns a guard that must be held for the application lifetime. -/// -/// # Errors -/// -/// Returns an error if no writable log directory can be found and stderr -/// fallback is not acceptable for your use case. -pub fn init_observability( - cfg: &ObservabilityConfig, - env_filter: EnvFilter, -) -> Result { - let (log_writer, log_guard) = match build_log_writer(&cfg.service, cfg.log_dir.as_deref()) { - Ok(result) => result, - Err(err) => { - // IMPORTANT: Fall back to stderr, NOT stdout. - // stdout is reserved for application output (e.g., MCP servers). - eprintln!("Warning: {err}. Falling back to stderr logging."); - let (writer, guard) = tracing_appender::non_blocking(std::io::stderr()); - (writer, guard) - } - }; - - let log_layer = JsonLogLayer::new(log_writer); - - tracing_subscriber::registry() - .with(env_filter) - .with(log_layer) - .init(); - - tracing::debug!("observability initialized"); - - Ok(ObservabilityGuard { - _log_guard: log_guard, - }) -} - -/// Build an `EnvFilter` based on CLI flags and environment. -/// -/// Priority: quiet flag > verbose flag > RUST_LOG env > default_level -pub fn env_filter(quiet: bool, verbose: u8, default_level: &str) -> EnvFilter { - if quiet { - return EnvFilter::new("error"); - } - - if verbose > 0 { - let level = match verbose { - 1 => "debug", - _ => "trace", - }; - return EnvFilter::new(level); - } - - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level)) -} - -// ============================================================================ -// JSON Log Layer -// ============================================================================ - -struct JsonLogLayer { - writer: W, -} - -impl JsonLogLayer { - const fn new(writer: W) -> Self { - Self { writer } - } -} - -impl tracing_subscriber::Layer for JsonLogLayer -where - S: tracing::Subscriber + for<'a> LookupSpan<'a>, - W: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer> + Send + Sync + 'static, -{ - fn on_new_span( - &self, - attrs: &tracing::span::Attributes<'_>, - id: &tracing::span::Id, - ctx: LayerContext<'_, S>, - ) { - if let Some(span) = ctx.span(id) { - let mut visitor = JsonVisitor::default(); - attrs.record(&mut visitor); - span.extensions_mut().insert(SpanFields { - values: visitor.values, - }); - } - } - - fn on_record( - &self, - id: &tracing::span::Id, - values: &tracing::span::Record<'_>, - ctx: LayerContext<'_, S>, - ) { - if let Some(span) = ctx.span(id) { - let mut visitor = JsonVisitor::default(); - values.record(&mut visitor); - let mut extensions = span.extensions_mut(); - if let Some(fields) = extensions.get_mut::() { - fields.values.extend(visitor.values); - } else { - extensions.insert(SpanFields { - values: visitor.values, - }); - } - } - } - - fn on_event(&self, event: &Event<'_>, ctx: LayerContext<'_, S>) { - let mut map = Map::new(); - - let timestamp = format_timestamp(); - map.insert("timestamp".to_string(), Value::String(timestamp)); - map.insert( - "level".to_string(), - Value::String(event.metadata().level().as_str().to_lowercase()), - ); - map.insert( - "target".to_string(), - Value::String(event.metadata().target().to_string()), - ); - - // Include span context in log entries - if let Some(scope) = ctx.event_scope(event) { - for span in scope.from_root() { - if let Some(fields) = span.extensions().get::() { - map.extend(fields.values.clone()); - } - } - } - - let mut visitor = JsonVisitor::default(); - event.record(&mut visitor); - map.extend(visitor.values); - - let mut writer = self.writer.make_writer(); - if serde_json::to_writer(&mut writer, &Value::Object(map)).is_ok() { - let _ = writer.write_all(b"\n"); - } - } -} - -#[derive(Clone, Debug)] -struct SpanFields { - values: Map, -} - -#[derive(Default)] -struct JsonVisitor { - values: Map, -} - -impl tracing::field::Visit for JsonVisitor { - fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { - self.values - .insert(field.name().to_string(), Value::Bool(value)); - } - - fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { - self.values - .insert(field.name().to_string(), Value::Number(value.into())); - } - - fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { - self.values - .insert(field.name().to_string(), Value::Number(value.into())); - } - - fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { - if let Some(number) = serde_json::Number::from_f64(value) { - self.values - .insert(field.name().to_string(), Value::Number(number)); - } - } - - fn record_str(&mut self, field: &tracing::field::Field, value: &str) { - self.values - .insert(field.name().to_string(), Value::String(value.to_string())); - } - - fn record_error( - &mut self, - field: &tracing::field::Field, - value: &(dyn std::error::Error + 'static), - ) { - self.values - .insert(field.name().to_string(), Value::String(value.to_string())); - } - - fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { - self.values.insert( - field.name().to_string(), - Value::String(format!("{value:?}")), - ); - } -} - -/// Format timestamp as RFC 3339 using std::time (no external time crate needed). -fn format_timestamp() -> String { - // Use std::time for basic timestamp - good enough for logging - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - - let secs = now.as_secs(); - let nanos = now.subsec_nanos(); - - // Convert to datetime components (UTC) - let days_since_epoch = secs / 86400; - let secs_of_day = secs % 86400; - let hours = secs_of_day / 3600; - let minutes = (secs_of_day % 3600) / 60; - let seconds = secs_of_day % 60; - - // Calculate year/month/day from days since epoch (1970-01-01) - let (year, month, day) = days_to_ymd(days_since_epoch as i64); - - format!( - "{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z", - millis = nanos / 1_000_000 - ) -} - -/// Convert days since Unix epoch to (year, month, day). -const fn days_to_ymd(days: i64) -> (i32, u32, u32) { - // Algorithm from Howard Hinnant's date algorithms - let z = days + 719468; - let era = if z >= 0 { z } else { z - 146096 } / 146097; - let doe = (z - era * 146097) as u32; - let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - let y = yoe as i64 + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; - let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let y = if m <= 2 { y + 1 } else { y }; - (y as i32, m, d) -} - -// ============================================================================ -// Log Target Resolution -// ============================================================================ - -fn build_log_writer( - service: &str, - config_log_dir: Option<&Path>, -) -> Result<( - tracing_appender::non_blocking::NonBlocking, - tracing_appender::non_blocking::WorkerGuard, -)> { - let target = resolve_log_target(service, config_log_dir).map_err(|e| anyhow::anyhow!("{e}"))?; - - let appender = tracing_appender::rolling::daily(&target.dir, &target.file_name); - let (writer, guard) = tracing_appender::non_blocking(appender); - - Ok((writer, guard)) -} - -fn resolve_log_target(service: &str, config_log_dir: Option<&Path>) -> Result { - let path_override = std::env::var_os(ENV_LOG_PATH).map(PathBuf::from); - let dir_override = std::env::var_os(ENV_LOG_DIR).map(PathBuf::from); - - resolve_log_target_with( - service, - path_override, - dir_override, - config_log_dir.map(PathBuf::from), - ) -} - -fn resolve_log_target_with( - service: &str, - path_override: Option, - dir_override: Option, - config_dir: Option, -) -> Result { - if let Some(path) = path_override { - return log_target_from_path(path); - } - - if let Some(dir) = dir_override { - return log_target_from_dir(dir, service); - } - - if let Some(dir) = config_dir { - return log_target_from_dir(dir, service); - } - - let mut candidates = Vec::new(); - - if cfg!(unix) { - candidates.push(PathBuf::from(DEFAULT_LOG_DIR_UNIX)); - } - - // Use XDG-compliant data directory for log storage - if let Some(proj_dirs) = directories::ProjectDirs::from("", "", service) { - candidates.push(proj_dirs.data_local_dir().join("logs")); - } - - if let Ok(dir) = std::env::current_dir() { - candidates.push(dir); - } - - let file_name = format!("{service}{LOG_FILE_SUFFIX}"); - - for dir in candidates { - if ensure_writable(&dir, &file_name).is_ok() { - return Ok(LogTarget { dir, file_name }); - } - } - - Err("No writable log directory found".to_string()) -} - -fn log_target_from_dir(dir: PathBuf, service: &str) -> Result { - let file_name = format!("{service}{LOG_FILE_SUFFIX}"); - ensure_writable(&dir, &file_name)?; - Ok(LogTarget { dir, file_name }) -} - -fn log_target_from_path(path: PathBuf) -> Result { - let file_name = path - .file_name() - .ok_or_else(|| format!("{ENV_LOG_PATH} must include a file name")) - .and_then(|name| { - name.to_str() - .map(|value| value.to_string()) - .ok_or_else(|| format!("{ENV_LOG_PATH} must be valid UTF-8")) - })?; - - let dir = path.parent().unwrap_or_else(|| Path::new(".")); - ensure_writable(dir, &file_name)?; - - Ok(LogTarget { - dir: dir.to_path_buf(), - file_name, - }) -} - -fn ensure_writable(dir: &Path, file_name: &str) -> Result<(), String> { - std::fs::create_dir_all(dir) - .map_err(|e| format!("Failed to create log directory {}: {e}", dir.display()))?; - - let path = dir.join(file_name); - OpenOptions::new() - .create(true) - .append(true) - .open(&path) - .map_err(|e| format!("Failed to open log file {}: {e}", path.display()))?; - - Ok(()) -} - -// ============================================================================ -// Tests -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn env_filter_quiet_overrides() { - let filter = env_filter(true, 0, "info"); - assert_eq!(filter.to_string(), "error"); - } - - #[test] - fn env_filter_verbose_maps_to_debug_and_trace() { - let debug_filter = env_filter(false, 1, "info"); - assert_eq!(debug_filter.to_string(), "debug"); - - let trace_filter = env_filter(false, 2, "info"); - assert_eq!(trace_filter.to_string(), "trace"); - } - - #[test] - fn log_target_from_path_uses_parent_dir() { - let temp_dir = std::env::temp_dir().join("colophon-log-path"); - let file_path = temp_dir.join("custom.jsonl"); - - let target = log_target_from_path(file_path).expect("log target from path"); - assert_eq!(target.dir, temp_dir); - assert_eq!(target.file_name, "custom.jsonl"); - } - - #[test] - fn log_target_from_dir_appends_file_name() { - let temp_dir = std::env::temp_dir().join("colophon-log-dir"); - let target = log_target_from_dir(temp_dir.clone(), "demo").expect("log target from dir"); - assert_eq!(target.dir, temp_dir); - assert_eq!(target.file_name, format!("demo{LOG_FILE_SUFFIX}")); - } - - #[test] - fn resolve_log_target_with_prefers_path_override() { - let temp_dir = std::env::temp_dir().join("colophon-log-override"); - let file_path = temp_dir.join("override.jsonl"); - - let target = resolve_log_target_with("demo", Some(file_path.clone()), None, None) - .expect("override log target"); - - assert_eq!(target.path(), file_path); - } - - #[test] - fn resolve_log_target_with_falls_back_to_dir_override() { - let temp_dir = std::env::temp_dir().join("colophon-log-dir-override"); - let target = resolve_log_target_with("demo", None, Some(temp_dir.clone()), None) - .expect("dir override log target"); - - assert_eq!(target.dir, temp_dir); - assert_eq!(target.file_name, format!("demo{LOG_FILE_SUFFIX}")); - } - - #[test] - fn resolve_log_target_with_uses_config_dir() { - let temp_dir = std::env::temp_dir().join("colophon-log-config-dir"); - let target = resolve_log_target_with("demo", None, None, Some(temp_dir.clone())) - .expect("config dir log target"); - - assert_eq!(target.dir, temp_dir); - assert_eq!(target.file_name, format!("demo{LOG_FILE_SUFFIX}")); - } - - #[test] - fn format_timestamp_produces_valid_rfc3339() { - let ts = format_timestamp(); - // Basic format check: YYYY-MM-DDTHH:MM:SS.mmmZ - assert!(ts.ends_with('Z'), "timestamp should end with Z: {ts}"); - assert_eq!(ts.len(), 24, "timestamp should be 24 chars: {ts}"); - assert_eq!(&ts[4..5], "-", "year-month separator"); - assert_eq!(&ts[7..8], "-", "month-day separator"); - assert_eq!(&ts[10..11], "T", "date-time separator"); - } - - #[test] - fn days_to_ymd_known_dates() { - // 1970-01-01 = day 0 - assert_eq!(days_to_ymd(0), (1970, 1, 1)); - // 2000-01-01 = day 10957 - assert_eq!(days_to_ymd(10957), (2000, 1, 1)); - // 2024-02-29 (leap year) = day 19782 - assert_eq!(days_to_ymd(19782), (2024, 2, 29)); - } -} diff --git a/crates/colophon/Cargo.toml b/crates/colophon/Cargo.toml index 18b415b..81aa7bc 100644 --- a/crates/colophon/Cargo.toml +++ b/crates/colophon/Cargo.toml @@ -39,6 +39,14 @@ pkg-fmt = "zip" [dependencies] colophon-core = { version = "0.1", path = "../colophon-core" } clap = { version = "4.6", features = ["derive"] } +librebar = { version = "0.6", features = [ + "cli", + "config", + "logging", + "crash", + "diagnostics", + "update", +] } anyhow = "1.0" tracing = "0.1" serde = { version = "1.0", features = ["derive"] } @@ -46,7 +54,6 @@ serde_json = "1.0" serde_yaml = "0.9" owo-colors = { version = "4.3", features = ["supports-colors"] } camino = { version = "1.2", features = ["serde1"] } -directories = "6.0" indicatif = "0.18.4" tabled = { version = "0.21.0", default-features = false, features = ["std", "macros"] } diff --git a/crates/colophon/src/lib.rs b/crates/colophon/src/lib.rs index dba3e0e..c313338 100644 --- a/crates/colophon/src/lib.rs +++ b/crates/colophon/src/lib.rs @@ -12,37 +12,12 @@ //! //! # Documentation Generation //! -//! The [`command()`] function returns the clap `Command` for generating man pages -//! and shell completions via `xtask`. +//! `xtask` builds man pages and shell completions from [`Cli`] through +//! librebar's generators, which render the same augmented command tree that +//! `librebar::cli::parse_with` uses at runtime. pub mod commands; -use clap::{CommandFactory, Parser, Subcommand}; -use std::path::PathBuf; - -/// Color output preference. -#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)] -pub enum ColorChoice { - /// Detect terminal capabilities automatically. - #[default] - Auto, - /// Always emit colors. - Always, - /// Never emit colors. - Never, -} - -impl ColorChoice { - /// Configure global color output based on this choice. - /// - /// Call this once at startup to set the color mode. - pub fn apply(self) { - match self { - Self::Auto => {} // owo-colors auto-detects by default - Self::Always => owo_colors::set_override(true), - Self::Never => owo_colors::set_override(false), - } - } -} +use librebar::cli::clap::{Parser, Subcommand}; const BANNER: &str = "\ ┌─┐┌─┐┬ ┌─┐┌─┐┬ ┬┌─┐┌┐┌ @@ -63,39 +38,19 @@ ENVIRONMENT VARIABLES: #[command(version, arg_required_else_help = true)] #[command(before_help = BANNER)] #[command(after_help = ENV_HELP)] -#[command(disable_help_flag = true)] pub struct Cli { + /// Flags shared by every librebar-based CLI. + /// + /// Supplies `-q`, `-v`, `-C`, `-c`, `--color`, `--format`, and + /// `--version-only`, all global. colophon declares none of these itself: + /// redeclaring one is a clap name collision, which panics at startup + /// rather than failing to compile. + #[command(flatten)] + pub common: librebar::cli::CommonArgs, + /// The subcommand to execute. #[command(subcommand)] pub command: Option, - - /// Print only the version number (for scripting) - #[arg(long)] - pub version_only: bool, - - /// Path to configuration file (overrides discovery) - #[arg(short, long, global = true, value_name = "FILE")] - pub config: Option, - - /// Run as if started in DIR - #[arg(short = 'C', long, global = true)] - pub chdir: Option, - - /// Only print errors (suppresses warnings/info) - #[arg(short, long, global = true)] - pub quiet: bool, - - /// More detail (repeatable; e.g. -vv) - #[arg(short, long, global = true, action = clap::ArgAction::Count)] - pub verbose: u8, - - /// Colorize output - #[arg(long, global = true, value_enum, default_value_t)] - pub color: ColorChoice, - - /// Output as JSON (for scripting) - #[arg(long, global = true)] - pub json: bool, } /// Available subcommands for the CLI. @@ -113,19 +68,15 @@ pub enum Commands { Render(commands::render::RenderArgs), } -/// Returns the clap command for documentation generation. +/// Machine-readable declaration of colophon's exit contract. /// -/// Adds a custom `-h`/`--help` flag using `HelpShort` so both render -/// the compact single-line format. This is done at the Command level -/// (not as a struct field) because clap's derive treats `HelpShort` -/// as a value-less exit action that conflicts with struct population. -pub fn command() -> clap::Command { - Cli::command().arg( - clap::Arg::new("help") - .short('h') - .long("help") - .help("Print help") - .global(true) - .action(clap::ArgAction::HelpShort), +/// Consumed by `librebar::cli::parse_with`, which validates it and serves it +/// from the `schema` subcommand. +pub fn schema_metadata() -> librebar::cli::SchemaMetadata { + librebar::cli::SchemaMetadata::new().error( + librebar::cli::ErrorMetadata::new("config_invalid") + .exit_code(2) + .retryable(false) + .description("Configuration could not be loaded or deserialized"), ) } diff --git a/crates/colophon/src/main.rs b/crates/colophon/src/main.rs index b12a340..64e6ea9 100644 --- a/crates/colophon/src/main.rs +++ b/crates/colophon/src/main.rs @@ -2,20 +2,18 @@ #![deny(unsafe_code)] use anyhow::Context; -use clap::FromArgMatches; use colophon::{Cli, Commands, commands}; -use colophon_core::config::ConfigLoader; use tracing::debug; -use colophon_core::observability; fn main() -> anyhow::Result<()> { - let cli = Cli::from_arg_matches(&colophon::command().get_matches()) - .expect("clap mismatch between Cli derive and command()"); + // `parse_with` answers `schema` and `completions` before config, logging, + // or any other work happens. + let cli = librebar::cli::parse_with::(colophon::schema_metadata()); - cli.color.apply(); - - if cli.version_only { - println!("{}", env!("CARGO_PKG_VERSION")); + // Applies color, then --version-only, then -C, in that fixed order: the + // version query cannot be broken by an unrelated bad -C, and the directory + // change lands before config discovery walks up from cwd. + if cli.common.apply(env!("CARGO_PKG_VERSION"))?.is_exit() { return Ok(()); } @@ -24,10 +22,10 @@ fn main() -> anyhow::Result<()> { return Ok(()); }; - if let Some(ref dir) = cli.chdir { - std::env::set_current_dir(dir) - .with_context(|| format!("failed to change directory to {}", dir.display()))?; - } + let json = matches!( + cli.common.output_format(), + librebar::cli::ResolvedOutputFormat::Json + ); let cwd = std::env::current_dir().context("failed to determine current directory")?; let cwd = camino::Utf8PathBuf::try_from(cwd).map_err(|e| { @@ -36,47 +34,44 @@ fn main() -> anyhow::Result<()> { e.into_path_buf().display() ) })?; - let mut loader = ConfigLoader::new().with_project_search(&cwd); - if let Some(ref config_path) = cli.config { - let config_path = camino::Utf8PathBuf::try_from(config_path.clone()).map_err(|e| { - anyhow::anyhow!( - "config path is not valid UTF-8: {}", - e.into_path_buf().display() - ) - })?; - loader = loader.with_file(&config_path); + let config_path = cli.common.config_path()?; + + let mut loader = colophon_core::ConfigLoader::new().with_project_search(&cwd); + if let Some(ref path) = config_path { + loader = loader.with_file(path); } let (config, config_sources) = loader.load().context("failed to load configuration")?; - let obs_config = observability::ObservabilityConfig::from_env_with_overrides( - config - .log_dir - .as_ref() - .map(|dir| dir.as_std_path().to_path_buf()), - ); - let env_filter = observability::env_filter(cli.quiet, cli.verbose, config.log_level.as_str()); - let _guard = observability::init_observability(&obs_config, env_filter) - .context("failed to initialize logging/tracing")?; + // Hold `app` for the rest of main: dropping it flushes and closes the log + // writer. librebar reads log_dir and log_level off the config we hand it, + // so colophon-core stays authoritative over config semantics. + let app = librebar::init(env!("CARGO_PKG_NAME")) + .with_version(env!("CARGO_PKG_VERSION")) + .with_cli(cli.common) + .with_config(config) + .logging() + .crash_handler() + .start() + .context("failed to initialize logging and crash handling")?; + + let config = app.config().clone(); debug!( - verbose = cli.verbose, - quiet = cli.quiet, - json = cli.json, - color = ?cli.color, - chdir = ?cli.chdir, + verbose = app.cli().verbose, + quiet = app.cli().quiet, + json, "CLI initialized" ); // Execute command let result = match command { - Commands::Curate(args) => commands::curate::cmd_curate(args, cli.json, &config), - Commands::Doctor(args) => { - commands::doctor::cmd_doctor(args, cli.json, &config_sources, &cwd) - } - Commands::Extract(args) => commands::extract::cmd_extract(args, cli.json, &config), - Commands::Info(args) => commands::info::cmd_info(args, cli.json, &config, &config_sources), - Commands::Render(args) => commands::render::cmd_render(args, cli.json, &config), + Commands::Curate(args) => commands::curate::cmd_curate(args, json, &config), + Commands::Doctor(args) => commands::doctor::cmd_doctor(args, json, &config_sources, &cwd), + Commands::Extract(args) => commands::extract::cmd_extract(args, json, &config), + Commands::Info(args) => commands::info::cmd_info(args, json, &config, &config_sources), + Commands::Render(args) => commands::render::cmd_render(args, json, &config), }; + // Logged here rather than after `app` drops, because `app` owns the log writer. if let Err(ref err) = result { tracing::error!(error = %err, "fatal error"); } diff --git a/crates/colophon/tests/cli.rs b/crates/colophon/tests/cli.rs index b76b34e..06b1e1c 100644 --- a/crates/colophon/tests/cli.rs +++ b/crates/colophon/tests/cli.rs @@ -85,7 +85,11 @@ fn info_shows_package_name_and_version() { #[test] fn info_json_outputs_valid_json() { - let output = cmd().arg("info").arg("--json").assert().success(); + let output = cmd() + .arg("info") + .args(["--format", "json"]) + .assert() + .success(); let stdout = String::from_utf8_lossy(&output.get_output().stdout); let json: serde_json::Value = @@ -99,7 +103,7 @@ fn info_json_outputs_valid_json() { fn info_json_contains_expected_fields() { cmd() .arg("info") - .arg("--json") + .args(["--format", "json"]) .assert() .success() .stdout(predicate::str::contains("\"name\"")) @@ -112,7 +116,20 @@ fn info_help_shows_command_options() { .args(["info", "--help"]) .assert() .success() - .stdout(predicate::str::contains("--json")); + .stdout(predicate::str::contains("--format")); +} + +// ============================================================================= +// Schema Subcommand +// ============================================================================= + +#[test] +fn schema_subcommand_outputs_json() { + cmd() + .arg("schema") + .assert() + .success() + .stdout(predicate::str::contains("\"name\": \"colophon\"")); } // ============================================================================= @@ -250,6 +267,8 @@ fn extract_produces_yaml_output() { .args([ "-C", tmp.path().to_str().unwrap(), + "--format", + "text", "extract", "--dir", ".", diff --git a/record/superpowers/plans/2026-08-11-librebar-adoption.md b/record/superpowers/plans/2026-08-11-librebar-adoption.md new file mode 100644 index 0000000..c9a0ea1 --- /dev/null +++ b/record/superpowers/plans/2026-08-11-librebar-adoption.md @@ -0,0 +1,654 @@ +# librebar 0.6 Adoption — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace hand-rolled CLI flags, config loading, and observability with librebar 0.6 — gaining crash dumps, update checks, CLI Spec schema, and diagnostics framework for free. + +**Architecture:** Split librebar across the workspace like bito: `colophon-core` gets `config` only (no process-global state), `colophon` (CLI binary) gets `cli`, `config`, `logging`, `crash`, `diagnostics`, `update`. The xtask gets `cli` only for completions/manpages via librebar generators. Config types stay in colophon-core — only the loading mechanism changes. + +**Tech Stack:** librebar 0.6, clap 4.6 (through `librebar::cli::clap` re-export), camino 1.2 + +**Reference implementation:** `~/source/claylo/bito` — completed this migration 2026-08-07. + +--- + +## File Structure + +### Files to Create +- None — this is a replacement, not an addition. + +### Files to Modify +- `crates/colophon-core/Cargo.toml` — add librebar, remove figment, directories, tracing-subscriber, tracing-appender +- `crates/colophon-core/src/config.rs` — replace `ConfigLoader` with thin wrapper over `librebar::config::ConfigLoader` +- `crates/colophon-core/src/error.rs` — change `ConfigError::Deserialize` from `figment::Error` to `Box` +- `crates/colophon-core/src/observability.rs` — delete all contents (becomes empty or removed) +- `crates/colophon-core/src/lib.rs` — remove `pub mod observability` +- `crates/colophon/Cargo.toml` — add librebar, remove direct clap (use re-export) +- `crates/colophon/src/lib.rs` — replace `Cli` struct with `CommonArgs` flatten, replace `command()` with `parse_with`, add schema metadata +- `crates/colophon/src/main.rs` — rewrite to librebar init builder pattern +- `crates/colophon/src/commands/mod.rs` — no changes needed (banner stays) +- `crates/colophon/src/commands/doctor.rs` — use `librebar::config` for directory resolution +- `crates/colophon/src/commands/info.rs` — adapt to new config/CLI types +- `crates/colophon/src/commands/extract.rs` — adapt `json` bool to output format enum +- `crates/colophon/src/commands/curate.rs` — adapt `json` bool to output format enum +- `crates/colophon/src/commands/render.rs` — adapt `json` bool to output format enum +- `crates/colophon/tests/cli.rs` — update for new flag behavior (--format replaces --json) +- `crates/colophon/tests/config_integration.rs` — update for new config loader API +- `xtask/Cargo.toml` — add librebar cli feature, remove clap_complete and clap_mangen +- `xtask/src/main.rs` — switch to librebar generators +- `xtask/src/commands/completions.rs` — use librebar completion generator +- `xtask/src/commands/man.rs` — use librebar manpage generator + +### Files to Delete +- `crates/colophon-core/src/observability.rs` — replaced by `librebar::logging` + +--- + +## Task 1: Add librebar to colophon-core, replace config loader + +This is the foundation. Replace figment-based `ConfigLoader` with a thin wrapper over `librebar::config::ConfigLoader`. Config types (`Config`, `SourceConfig`, `ExtractConfig`, etc.) stay exactly as they are — only the loading mechanism changes. + +**Files:** +- Modify: `crates/colophon-core/Cargo.toml` +- Modify: `crates/colophon-core/src/config.rs` +- Modify: `crates/colophon-core/src/error.rs` + +- [ ] **Step 1: Update colophon-core dependencies** + +In `crates/colophon-core/Cargo.toml`, add librebar and remove figment and directories: + +```toml +# Replace this: +figment = { version = "0.10", features = ["toml", "yaml", "json"] } +# ... +directories = "6.0" + +# With this: +librebar = { version = "0.6", default-features = false, features = ["config"] } +``` + +Keep all other dependencies unchanged. + +- [ ] **Step 2: Update ConfigError to not leak figment** + +In `crates/colophon-core/src/error.rs`, change the `Deserialize` variant: + +```rust +// Before: +#[error("invalid configuration: {0}")] +Deserialize(#[from] Box), + +// After: +#[error("invalid configuration: {0}")] +Deserialize(Box), +``` + +Remove the `#[from]` since we'll construct it explicitly. + +- [ ] **Step 3: Rewrite ConfigLoader as a thin wrapper** + +Replace the `ConfigLoader` struct and its methods in `crates/colophon-core/src/config.rs`. Remove all figment imports. The new loader delegates to librebar: + +```rust +use librebar::config::ConfigLoader as LibrebarLoader; + +const APP_NAME: &str = "colophon"; + +pub struct ConfigLoader { + inner: LibrebarLoader, + explicit_files: Vec, +} + +impl ConfigLoader { + pub fn new() -> Self { + Self { + inner: LibrebarLoader::new(APP_NAME), + explicit_files: Vec::new(), + } + } + + pub fn with_project_search>(mut self, path: P) -> Self { + self.inner = self.inner.with_project_search(path); + self + } + + pub fn with_file>(mut self, path: P) -> Self { + self.explicit_files.push(path.as_ref().to_path_buf()); + self.inner = self.inner.with_file(path); + self + } + + pub fn with_user_config(mut self, include: bool) -> Self { + if !include { + self.inner = self.inner.without_user_config(); + } + self + } + + pub fn with_boundary_marker>(mut self, marker: S) -> Self { + self.inner = self.inner.with_boundary_marker(marker); + self + } + + pub fn without_boundary_marker(mut self) -> Self { + self.inner = self.inner.without_boundary_marker(); + self + } + + #[tracing::instrument(skip(self))] + pub fn load(self) -> ConfigResult<(Config, ConfigSources)> { + let (config, lb_sources) = self.inner + .load::() + .map_err(|e| ConfigError::Deserialize(Box::new(e)))?; + + let sources = ConfigSources { + project_file: lb_sources.project_file().map(Utf8PathBuf::from), + user_file: lb_sources.user_file().map(Utf8PathBuf::from), + explicit_files: self.explicit_files, + }; + + Ok((config, sources)) + } + + pub fn load_or_error(self) -> ConfigResult<(Config, ConfigSources)> { + // librebar's load already returns an error if no config found + // when using load_required; but we match our existing API + self.load() + } +} +``` + +Note: Check librebar's actual `ConfigSources` API — the exact accessor names (`project_file()`, `user_file()`) need to match what librebar exposes. Reference `~/source/claylo/bito/crates/bito-core/src/config.rs` for the working pattern. + +- [ ] **Step 4: Replace XDG directory helpers** + +Replace the `project_dirs()`, `user_config_dir()`, `user_cache_dir()`, `user_data_dir()`, `user_data_local_dir()` functions with librebar delegations: + +```rust +pub fn user_config_dir() -> Option { + librebar::config::user_config_dir(APP_NAME) +} + +pub fn user_cache_dir() -> Option { + librebar::config::user_cache_dir(APP_NAME) +} + +pub fn user_data_dir() -> Option { + librebar::config::user_data_dir(APP_NAME) +} + +pub fn user_data_local_dir() -> Option { + librebar::config::user_data_local_dir(APP_NAME) +} +``` + +Check librebar's actual function signatures — they may take `&str` or return `Utf8PathBuf` directly. Reference bito-core's config.rs for the exact signatures. + +- [ ] **Step 5: Remove stale helpers** + +Delete: +- The `find_project_config()` method +- The `find_user_config()` method +- The `merge_file()` method +- The `CONFIG_EXTENSIONS` constant +- The `project_dirs()` function + +These are all replaced by librebar's config discovery. + +- [ ] **Step 6: Run tests to verify config loading still works** + +```bash +cargo nextest run -p colophon-core -- config +``` + +All config tests should pass since the Config struct and its serde Deserialize impl are unchanged — only the loader changed. Fix any that break due to API differences. + +- [ ] **Step 7: Commit** + +``` +chore(config): replace figment with librebar config loader + +Thin wrapper over librebar::config::ConfigLoader preserves colophon's +existing ConfigLoader API. Config types unchanged. Removes figment +and directories as direct dependencies. +``` + +--- + +## Task 2: Delete observability module, add librebar to CLI crate + +Remove the hand-rolled JSONL logging layer and observability setup. The CLI crate will use `librebar::logging` via the init builder in Task 3. + +**Files:** +- Modify: `crates/colophon-core/src/lib.rs` +- Delete: `crates/colophon-core/src/observability.rs` +- Modify: `crates/colophon/Cargo.toml` + +- [ ] **Step 1: Remove observability module from core** + +In `crates/colophon-core/src/lib.rs`, remove: + +```rust +pub mod observability; +``` + +- [ ] **Step 2: Delete observability.rs** + +```bash +rm crates/colophon-core/src/observability.rs +``` + +- [ ] **Step 3: Remove tracing-subscriber and tracing-appender from colophon-core** + +In `crates/colophon-core/Cargo.toml`, remove: + +```toml +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" +``` + +Keep `tracing = "0.1"` — core still uses `tracing::debug!`, `tracing::instrument`, etc. + +- [ ] **Step 4: Update CLI crate dependencies** + +In `crates/colophon/Cargo.toml`: + +```toml +# Add: +librebar = { version = "0.6", features = [ + "cli", "config", "logging", "crash", "diagnostics", "update", +] } + +# Remove direct clap (use librebar's re-export): +# clap = { version = "4.6", features = ["derive"] } <-- delete this line +``` + +Keep: `owo-colors`, `indicatif`, `tabled`, `serde`, `serde_json`, `serde_yaml`, `anyhow`, `tracing`, `camino`. + +- [ ] **Step 5: Verify it compiles (will have errors in main.rs/lib.rs — expected)** + +```bash +cargo check -p colophon-core 2>&1 | head -20 +``` + +Core should compile clean. The colophon CLI crate will have errors because main.rs still references observability — that's fixed in Task 3. + +- [ ] **Step 6: Commit core changes only** + +``` +refactor(core): remove observability module + +Logging setup moves to the CLI binary via librebar::logging. +Core crate keeps tracing instrumentation but no longer owns +the subscriber or log writer. +``` + +--- + +## Task 3: Rewrite CLI struct and main.rs with librebar + +Replace the hand-rolled `Cli` struct with `CommonArgs` flatten, and rewrite `main.rs` to use the librebar init builder. + +**Files:** +- Modify: `crates/colophon/src/lib.rs` +- Modify: `crates/colophon/src/main.rs` + +- [ ] **Step 1: Rewrite lib.rs with CommonArgs** + +Replace the entire `Cli` struct and `command()` function. Key changes: +- Use `librebar::cli::clap::{Parser, Subcommand}` instead of direct clap +- Flatten `CommonArgs` instead of hand-rolling flags +- Add `schema_metadata()` function +- Remove `ColorChoice` enum (librebar handles it) +- Keep `BANNER` and `ENV_HELP` + +```rust +pub mod commands; + +use librebar::cli::clap::{Parser, Subcommand}; + +const BANNER: &str = "\ +┌─┐┌─┐┬ ┌─┐┌─┐┬ ┬┌─┐┌┐┌ +│ │ ││ │ │├─┘├─┤│ ││││ +└─┘└─┘┴─┘└─┘┴ ┴ ┴└─┘┘└┘"; + +const ENV_HELP: &str = "\ +ENVIRONMENT VARIABLES: + RUST_LOG Log filter (e.g., debug, colophon=trace) + COLOPHON_LOG_PATH Log file path (rotated daily) + COLOPHON_LOG_DIR Log directory +"; + +#[derive(Parser)] +#[command(name = "colophon")] +#[command(about = "Generate book indexes and glossaries from Markdown or Typst.", long_about = None)] +#[command(version, arg_required_else_help = true)] +#[command(before_help = BANNER)] +#[command(after_help = ENV_HELP)] +pub struct Cli { + #[command(flatten)] + pub common: librebar::cli::CommonArgs, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Curate extracted candidates into an index using Claude + Curate(commands::curate::CurateArgs), + /// Diagnose configuration and environment + Doctor(commands::doctor::DoctorArgs), + /// Extract keyword candidates from documents + Extract(commands::extract::ExtractArgs), + /// Show package information + Info(commands::info::InfoArgs), + /// Render curated terms as index markers in source files + Render(commands::render::RenderArgs), +} + +pub fn schema_metadata() -> librebar::cli::SchemaMetadata { + librebar::cli::SchemaMetadata::new() + .error( + librebar::cli::ErrorMetadata::new("config_invalid") + .exit_code(2) + .retryable(false) + .description("Configuration could not be discovered, parsed, or deserialized"), + ) +} +``` + +**Important:** Check if `CommonArgs` includes `-c/--config`. If it does, colophon's existing `-c` (short for `--config`) won't collide — they're the same flag. But verify at runtime. + +- [ ] **Step 2: Rewrite main.rs with librebar init builder** + +```rust +#![deny(unsafe_code)] + +use anyhow::Context; +use colophon::{Cli, Commands, commands}; +use tracing::debug; + +fn main() -> anyhow::Result<()> { + let cli = librebar::cli::parse_with::(colophon::schema_metadata()); + + if cli.common.apply(env!("CARGO_PKG_VERSION"))?.is_exit() { + return Ok(()); + } + + let Some(command) = cli.command else { + return Ok(()); + }; + + let json = matches!( + cli.common.output_format(), + librebar::cli::ResolvedOutputFormat::Json + ); + + let cwd = std::env::current_dir().context("failed to determine current directory")?; + let cwd = camino::Utf8PathBuf::try_from(cwd).map_err(|e| { + anyhow::anyhow!( + "current directory is not valid UTF-8: {}", + e.into_path_buf().display() + ) + })?; + let config_path = cli.common.config_path()?; + + let mut loader = colophon_core::ConfigLoader::new().with_project_search(&cwd); + if let Some(ref path) = config_path { + loader = loader.with_file(path); + } + let (config, config_sources) = loader.load().context("failed to load configuration")?; + + let app = librebar::init(env!("CARGO_PKG_NAME")) + .with_version(env!("CARGO_PKG_VERSION")) + .with_cli(cli.common) + .with_config(config) + .logging() + .crash_handler() + .start() + .context("failed to initialize logging and crash handling")?; + + let config = app.config().clone(); + + debug!( + verbose = app.cli().verbose, + quiet = app.cli().quiet, + json, + "CLI initialized" + ); + + let result = match command { + Commands::Curate(args) => commands::curate::cmd_curate(args, json, &config), + Commands::Doctor(args) => { + commands::doctor::cmd_doctor(args, json, &config_sources, &cwd) + } + Commands::Extract(args) => commands::extract::cmd_extract(args, json, &config), + Commands::Info(args) => commands::info::cmd_info(args, json, &config, &config_sources), + Commands::Render(args) => commands::render::cmd_render(args, json, &config), + }; + if let Err(ref err) = result { + tracing::error!(error = %err, "fatal error"); + } + result +} +``` + +Note: `cli.common.apply()` handles `--color`, `--version-only`, and `--chdir` in the correct order — replacing colophon's manual `cli.color.apply()`, version check, and `set_current_dir`. `cli.common.config_path()` provides the `--config`/`-c` value. Check librebar's actual method names. + +- [ ] **Step 3: Verify it compiles** + +```bash +cargo check -p colophon +``` + +Fix any type mismatches. The most likely issues: +- `config_path()` return type may differ from what we expect +- `app.config()` returns `&C` not `C` — may need `.clone()` +- `app.cli()` field names for verbose/quiet + +- [ ] **Step 4: Commit** + +``` +refactor(cli): adopt librebar for CLI parsing and init + +Replace hand-rolled --quiet, --verbose, --color, --json, --chdir, +--config, and --version-only with librebar CommonArgs flatten. +Gains --format auto|text|json, CLI Spec schema subcommand, +crash dumps, and structured logging via librebar builder. +``` + +--- + +## Task 4: Update xtask for librebar generators + +Replace clap_complete and clap_mangen with librebar's built-in generators. + +**Files:** +- Modify: `xtask/Cargo.toml` +- Modify: `xtask/src/commands/completions.rs` +- Modify: `xtask/src/commands/man.rs` + +- [ ] **Step 1: Update xtask dependencies** + +In `xtask/Cargo.toml`: + +```toml +[dependencies] +clap = { version = "4.6", features = ["derive"] } +colophon = { path = "../crates/colophon" } +librebar = { version = "0.6", default-features = false, features = ["cli"] } +``` + +Remove `clap_complete` and `clap_mangen`. + +- [ ] **Step 2: Update completions command** + +In `xtask/src/commands/completions.rs`, use librebar's completion generator. Check bito's xtask for the exact API — it likely uses `librebar::cli::generate_completions::()` or similar. The pattern from bito's xtask is the reference. + +- [ ] **Step 3: Update man command** + +In `xtask/src/commands/man.rs`, use `librebar::cli::generate_manpages::()`. Filter out the `help` subcommand junk page (known gotcha from bito migration). + +- [ ] **Step 4: Verify xtask works** + +```bash +cargo xtask completions --out-dir /tmp/colophon-completions +cargo xtask man --out-dir /tmp/colophon-man +ls /tmp/colophon-completions/ /tmp/colophon-man/ +``` + +- [ ] **Step 5: Commit** + +``` +refactor(xtask): use librebar generators for completions and manpages + +Removes clap_complete and clap_mangen as direct dependencies. +``` + +--- + +## Task 5: Update integration tests + +The CLI behavior changes: `--json` becomes `--format json`, `-h`/`--help` both print compact help, and new subcommands (`schema`, `completions`) appear. + +**Files:** +- Modify: `crates/colophon/tests/cli.rs` +- Modify: `crates/colophon/tests/config_integration.rs` + +- [ ] **Step 1: Update cli.rs test helpers** + +The `cmd()` helper stays the same. Update tests that use `--json` to use `--format json`: + +```rust +// Before: +cmd().arg("extract").arg("--json").arg("--dir").arg(...) +// After: +cmd().arg("extract").arg("--format").arg("json").arg("--dir").arg(...) +``` + +- [ ] **Step 2: Update help/version tests** + +Both `-h` and `--help` now print compact help (same output). The `schema` and `completions` subcommands appear in help text. Add a test: + +```rust +#[test] +fn schema_subcommand_outputs_json() { + cmd() + .arg("schema") + .assert() + .success() + .stdout(predicate::str::contains("\"name\": \"colophon\"")); +} +``` + +- [ ] **Step 3: Update config_integration.rs** + +The config loader API is unchanged (thin wrapper), so most tests should pass. If any tests construct a `ConfigLoader` with methods that changed names, update them. + +- [ ] **Step 4: Run full test suite** + +```bash +cargo nextest run +``` + +Fix any remaining failures. + +- [ ] **Step 5: Commit** + +``` +test: update integration tests for librebar CLI changes + +--json replaced by --format json, schema subcommand added, +help output unified for -h and --help. +``` + +--- + +## Task 6: Update command implementations for output format + +Each command currently takes `json: bool`. This still works since we compute `json` from `output_format()` in main.rs. But verify each command handles the banner suppression correctly — librebar's CommonArgs may handle this differently. + +**Files:** +- Modify: `crates/colophon/src/commands/mod.rs` (if banner logic needs updating) +- Modify: `crates/colophon/src/commands/doctor.rs` +- Modify: `crates/colophon/src/commands/info.rs` + +- [ ] **Step 1: Update doctor to use librebar config dirs** + +In `commands/doctor.rs`, replace `config::user_config_dir()` etc. with the versions from colophon-core (which now delegate to librebar). These should be the same function names — verify they still work. + +- [ ] **Step 2: Verify banner suppression** + +The banner in `commands/mod.rs` checks `stderr().is_terminal()`. With `--format auto`, piped output auto-detects JSON — but the banner is on stderr, so it should still show. Verify this is correct behavior. + +- [ ] **Step 3: Run the full check suite** + +```bash +just check +``` + +All 269+ tests should pass, clippy clean, deny clean. + +- [ ] **Step 4: Commit** + +``` +refactor(commands): adapt to librebar output format +``` + +--- + +## Task 7: Clean up removed dependencies from workspace + +Final cleanup — verify the lockfile is clean and no stale deps remain. + +**Files:** +- Modify: `Cargo.lock` (via `cargo update`) + +- [ ] **Step 1: Run cargo update** + +```bash +cargo update +``` + +Verify figment, directories, tracing-subscriber, tracing-appender, clap_complete, and clap_mangen are gone from the lockfile. + +- [ ] **Step 2: Run full verification** + +```bash +just check +just outdated +``` + +- [ ] **Step 3: Verify new features work** + +```bash +# CLI Spec schema +cargo run -- schema + +# Crash dump (trigger with RUST_BACKTRACE=1 on a bad input) +# Update check (runs on info/doctor) +cargo run -- info +cargo run -- doctor +``` + +- [ ] **Step 4: Commit** + +``` +chore(deps): remove stale dependencies after librebar adoption + +Removes figment, directories, tracing-subscriber, tracing-appender, +clap_complete, clap_mangen. librebar 0.6 provides all of these. +``` + +--- + +## Gotchas Checklist (from bito's migration) + +Before starting, read `~/source/claylo/bito/record/superpowers/plans/2026-08-03-librebar-adoption.md` for the full "what actually happened" notes. Key traps: + +- [ ] **Flag collision**: `CommonArgs` owns `-c/--config`. Colophon has `-c` as short for `--config` — should be the same flag, but verify no panic. +- [ ] **Config key names**: librebar reads `log_dir`, `log_level`, `log_retention_days` off the config struct via serde. Colophon's `Config` uses `log_dir` and `log_level` (matching). Add a `log_retention_days` field or verify librebar doesn't require it. +- [ ] **`--format auto` piped behavior**: With `--format auto` (the default), piped stdout gets JSON. This is new behavior. Integration tests that pipe may break. +- [ ] **xtask junk manpages**: Filter out `colophon-help.1` from manpage generation. +- [ ] **Don't remove figment until the last import is gone**: Task 1 replaces the loader. Only then remove figment from Cargo.toml. diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 4c1f9e6..778d079 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -10,5 +10,5 @@ homepage.workspace = true [dependencies] clap = { version = "4.6", features = ["derive"] } clap_complete = "4.6" -clap_mangen = "0.3" colophon = { path = "../crates/colophon" } +librebar = { version = "0.6", default-features = false, features = ["cli"] } diff --git a/xtask/src/commands/completions.rs b/xtask/src/commands/completions.rs index bb34b6c..b8f46c3 100644 --- a/xtask/src/commands/completions.rs +++ b/xtask/src/commands/completions.rs @@ -19,7 +19,11 @@ pub fn cmd_completions(args: CompletionsArgs) -> Result<(), String> { let out_dir = crate::workspace_root().join(args.out_dir); fs::create_dir_all(&out_dir).map_err(|e| format!("{}: {e}", out_dir.display()))?; - let mut cmd = colophon::command(); + // Same augmented tree `librebar::cli::parse_with` uses, so completions cover + // `schema` and `completions` themselves. Fails only if the application + // declares a command name librebar reserves. + let mut cmd = librebar::cli::command::() + .map_err(|error| format!("build the augmented CLI command tree: {error}"))?; let bin_name = "colophon"; let shells: Vec = match args.shell { diff --git a/xtask/src/commands/man.rs b/xtask/src/commands/man.rs index 2f8aba4..c5658dc 100644 --- a/xtask/src/commands/man.rs +++ b/xtask/src/commands/man.rs @@ -1,4 +1,3 @@ -use std::fs; use std::path::PathBuf; use clap::Args; @@ -12,30 +11,17 @@ pub struct ManArgs { pub fn cmd_man(args: ManArgs) -> Result<(), String> { let out_dir = crate::workspace_root().join(args.out_dir); - fs::create_dir_all(&out_dir).map_err(|e| format!("{}: {e}", out_dir.display()))?; - // Generate main command manpage - let cmd = colophon::command(); - let man = clap_mangen::Man::new(cmd.clone()); - let mut buffer: Vec = Vec::new(); - man.render(&mut buffer) - .map_err(|e| format!("render manpage: {e}"))?; + // Renders the same augmented tree `librebar::cli::parse_with` uses, so the + // pages cover `schema` and `completions` too. Filenames carry the full + // hyphenated command path, so equally named leaves in different subtrees + // cannot overwrite each other. Creates the output directory itself, and + // skips Clap's generated `help` subcommand. + let paths = librebar::cli::generate_manpages::(&out_dir) + .map_err(|error| format!("generate manpages in {}: {error}", out_dir.display()))?; - let man_path = out_dir.join("colophon.1"); - fs::write(&man_path, buffer).map_err(|e| format!("{}: {e}", man_path.display()))?; - println!("wrote {}", man_path.display()); - - // Generate subcommand manpages - for subcommand in cmd.get_subcommands() { - let name = subcommand.get_name(); - let man = clap_mangen::Man::new(subcommand.clone()); - let mut buffer: Vec = Vec::new(); - man.render(&mut buffer) - .map_err(|e| format!("render manpage for {name}: {e}"))?; - - let man_path = out_dir.join(format!("colophon-{name}.1")); - fs::write(&man_path, buffer).map_err(|e| format!("{}: {e}", man_path.display()))?; - println!("wrote {}", man_path.display()); + for path in paths { + println!("wrote {}", path.display()); } Ok(())