Initial release: Citrine v1.0.0
Minimalist plaintext project-based todo CLI in Rust. Commands: init, add, list, set, done, rm. Supports .citrine + .citrine.local merging, partial hash matching, status filtering, and colorized output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
/dist
|
||||||
|
.claude
|
||||||
|
CITRINE_SPEC.md
|
||||||
980
Cargo.lock
generated
Normal file
980
Cargo.lock
generated
Normal file
@@ -0,0 +1,980 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "citrine"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"colored",
|
||||||
|
"rand",
|
||||||
|
"regex",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.85"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.182"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.115"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.1",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.2+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
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 = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "citrine"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "citrine"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
rand = "0.8"
|
||||||
|
regex = "1"
|
||||||
|
chrono = "0.4"
|
||||||
|
anyhow = "1"
|
||||||
|
colored = "2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
regex = "1"
|
||||||
81
README.md
Normal file
81
README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Citrine
|
||||||
|
|
||||||
|
Minimalist, plaintext, project-based todo CLI. Creates `.citrine` files in your project directory — human-readable, git-friendly, and AI-agent-parseable.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Download a binary from [releases](https://github.com/syntaxbullet/citrine/releases) or build from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
citrine init
|
||||||
|
citrine add "Implement caching layer"
|
||||||
|
citrine add "Fix mobile layout" --local
|
||||||
|
citrine list
|
||||||
|
citrine set a4f9 progress
|
||||||
|
citrine done a4f9
|
||||||
|
citrine rm a4f9
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `citrine init` | Initialize citrine in the current directory |
|
||||||
|
| `citrine add <description> [--local]` | Add a new task (`--local` for `.citrine.local`) |
|
||||||
|
| `citrine list [--status <s>] [--local]` | List tasks, optionally filtered |
|
||||||
|
| `citrine set <hash> <status>` | Change task status |
|
||||||
|
| `citrine done <hash>` | Mark task as done (shorthand for `set <hash> done`) |
|
||||||
|
| `citrine rm <hash>` | Remove a task |
|
||||||
|
|
||||||
|
**Statuses:** `todo`, `progress`, `blocked`, `done`
|
||||||
|
|
||||||
|
**Hashes** can be partial — `a4` works if it's unambiguous.
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## Backend
|
||||||
|
[a4f9] [ ] Implement caching layer @priority:high @backend
|
||||||
|
[b2e1] [>] Refactor auth module @assigned:cursor
|
||||||
|
[c3d8] [-] Deploy to staging @blocked:waiting-for-credentials
|
||||||
|
[f1a2] [x] Set up Docker config @completed:2025-02-14
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
[d4b3] [ ] Build settings page
|
||||||
|
[e5c2] [>] Fix mobile layout issues @priority:medium
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Symbols
|
||||||
|
|
||||||
|
| Symbol | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `[ ]` | Todo |
|
||||||
|
| `[>]` | In Progress |
|
||||||
|
| `[-]` | Blocked |
|
||||||
|
| `[x]` | Done |
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
|
||||||
|
Append `@tag` or `@key:value` to any task. Free-form, not validated. `@completed:YYYY-MM-DD` is auto-added when marking a task done.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **`.citrine`** — shared project tasks, commit to git
|
||||||
|
- **`.citrine.local`** — personal tasks, gitignored
|
||||||
|
|
||||||
|
Both share the same hash namespace. `citrine list` merges both by default.
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | Generic error |
|
||||||
|
| 2 | Invalid arguments |
|
||||||
|
| 3 | Not initialized |
|
||||||
35
src/commands/add.rs
Normal file
35
src/commands/add.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use crate::file::{collect_all_hashes, CitrineFile};
|
||||||
|
use crate::hash::generate_hash;
|
||||||
|
use crate::task::{Line, Status, Task};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn run(dir: &Path, description: &str, local: bool) -> Result<()> {
|
||||||
|
let citrine_path = dir.join(".citrine");
|
||||||
|
if !citrine_path.exists() {
|
||||||
|
anyhow::bail!("Not initialized. Run 'citrine init' first");
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing = collect_all_hashes(dir);
|
||||||
|
let hash = generate_hash(&existing)?;
|
||||||
|
|
||||||
|
let target = if local {
|
||||||
|
dir.join(".citrine.local")
|
||||||
|
} else {
|
||||||
|
citrine_path
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = CitrineFile::read_or_empty(&target);
|
||||||
|
let task = Task {
|
||||||
|
hash: hash.clone(),
|
||||||
|
status: Status::Todo,
|
||||||
|
description: description.to_string(),
|
||||||
|
metadata: String::new(),
|
||||||
|
};
|
||||||
|
file.lines.push(Line::Task(task));
|
||||||
|
file.write(&target)?;
|
||||||
|
|
||||||
|
let suffix = if local { " (local)" } else { "" };
|
||||||
|
println!("Added task [{}]: {}{}", hash, description, suffix);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
28
src/commands/init.rs
Normal file
28
src/commands/init.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn run(dir: &Path) -> Result<()> {
|
||||||
|
let citrine_path = dir.join(".citrine");
|
||||||
|
if !citrine_path.exists() {
|
||||||
|
fs::write(&citrine_path, "")?;
|
||||||
|
}
|
||||||
|
println!("Initialized citrine in {}", dir.display());
|
||||||
|
|
||||||
|
let gitignore_path = dir.join(".gitignore");
|
||||||
|
let entry = ".citrine.local";
|
||||||
|
|
||||||
|
if gitignore_path.exists() {
|
||||||
|
let content = fs::read_to_string(&gitignore_path)?;
|
||||||
|
if !content.lines().any(|l| l.trim() == entry) {
|
||||||
|
let separator = if content.ends_with('\n') || content.is_empty() { "" } else { "\n" };
|
||||||
|
fs::write(&gitignore_path, format!("{}{}{}\n", content, separator, entry))?;
|
||||||
|
println!("Added {} to .gitignore", entry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::write(&gitignore_path, format!("{}\n", entry))?;
|
||||||
|
println!("Added {} to .gitignore", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
74
src/commands/list.rs
Normal file
74
src/commands/list.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use crate::file::CitrineFile;
|
||||||
|
use crate::task::{Line, Status};
|
||||||
|
use anyhow::Result;
|
||||||
|
use colored::Colorize;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn run(dir: &Path, status_filter: Option<&str>, local_only: bool) -> Result<()> {
|
||||||
|
let citrine_path = dir.join(".citrine");
|
||||||
|
if !citrine_path.exists() {
|
||||||
|
anyhow::bail!("Not initialized. Run 'citrine init' first");
|
||||||
|
}
|
||||||
|
|
||||||
|
let filter = match status_filter {
|
||||||
|
Some(s) => Some(Status::from_name(s).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Invalid status '{}'. Valid statuses: todo, progress, blocked, done", s)
|
||||||
|
})?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let files: Vec<CitrineFile> = if local_only {
|
||||||
|
vec![CitrineFile::read_or_empty(&dir.join(".citrine.local"))]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
CitrineFile::read_or_empty(&citrine_path),
|
||||||
|
CitrineFile::read_or_empty(&dir.join(".citrine.local")),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut has_output = false;
|
||||||
|
for file in &files {
|
||||||
|
for line in &file.lines {
|
||||||
|
match line {
|
||||||
|
Line::Task(t) => {
|
||||||
|
if filter.as_ref().map_or(true, |f| t.status == *f) {
|
||||||
|
let status_str = match t.status {
|
||||||
|
Status::Todo => format!("[{}]", t.status.symbol()),
|
||||||
|
Status::InProgress => format!("[{}]", t.status.symbol()).yellow().to_string(),
|
||||||
|
Status::Blocked => format!("[{}]", t.status.symbol()).red().to_string(),
|
||||||
|
Status::Done => format!("[{}]", t.status.symbol()).green().to_string(),
|
||||||
|
};
|
||||||
|
let meta = if t.metadata.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" {}", t.metadata.dimmed())
|
||||||
|
};
|
||||||
|
println!("[{}] {} {}{}", t.hash, status_str, t.description, meta);
|
||||||
|
has_output = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Line::Section(s) => {
|
||||||
|
if filter.is_none() {
|
||||||
|
println!("{}", format!("## {}", s).bold());
|
||||||
|
has_output = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Line::Other(s) if !s.trim().is_empty() && filter.is_none() => {
|
||||||
|
println!("{}", s);
|
||||||
|
has_output = true;
|
||||||
|
}
|
||||||
|
Line::Other(_) if filter.is_none() => {
|
||||||
|
println!();
|
||||||
|
has_output = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_output && filter.is_some() {
|
||||||
|
println!("No tasks matching filter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
5
src/commands/mod.rs
Normal file
5
src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod init;
|
||||||
|
pub mod add;
|
||||||
|
pub mod list;
|
||||||
|
pub mod set;
|
||||||
|
pub mod rm;
|
||||||
29
src/commands/rm.rs
Normal file
29
src/commands/rm.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::file::{resolve_hash, CitrineFile};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn run(dir: &Path, partial_hash: &str) -> Result<()> {
|
||||||
|
let citrine_path = dir.join(".citrine");
|
||||||
|
if !citrine_path.exists() {
|
||||||
|
anyhow::bail!("Not initialized. Run 'citrine init' first");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut main_file = CitrineFile::read_or_empty(&citrine_path);
|
||||||
|
let mut local_file = CitrineFile::read_or_empty(&dir.join(".citrine.local"));
|
||||||
|
|
||||||
|
let (hash, is_local) = resolve_hash(&main_file, &local_file, partial_hash)?;
|
||||||
|
|
||||||
|
let file = if is_local { &mut local_file } else { &mut main_file };
|
||||||
|
let task = file.remove_task(&hash).unwrap();
|
||||||
|
|
||||||
|
println!("Removed [{}]: {}", hash, task.description);
|
||||||
|
|
||||||
|
let path = if is_local {
|
||||||
|
dir.join(".citrine.local")
|
||||||
|
} else {
|
||||||
|
citrine_path
|
||||||
|
};
|
||||||
|
file.write(&path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
49
src/commands/set.rs
Normal file
49
src/commands/set.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use crate::file::{resolve_hash, CitrineFile};
|
||||||
|
use crate::task::Status;
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::Local;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn run(dir: &Path, partial_hash: &str, status_name: &str) -> Result<()> {
|
||||||
|
let citrine_path = dir.join(".citrine");
|
||||||
|
if !citrine_path.exists() {
|
||||||
|
anyhow::bail!("Not initialized. Run 'citrine init' first");
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_status = Status::from_name(status_name).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Invalid status '{}'. Valid statuses: todo, progress, blocked, done", status_name)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut main_file = CitrineFile::read_or_empty(&citrine_path);
|
||||||
|
let mut local_file = CitrineFile::read_or_empty(&dir.join(".citrine.local"));
|
||||||
|
|
||||||
|
let (hash, is_local) = resolve_hash(&main_file, &local_file, partial_hash)?;
|
||||||
|
|
||||||
|
let file = if is_local { &mut local_file } else { &mut main_file };
|
||||||
|
let task = file.find_task_mut(&hash).unwrap();
|
||||||
|
|
||||||
|
let old_status = task.status.clone();
|
||||||
|
task.status = new_status.clone();
|
||||||
|
|
||||||
|
if new_status == Status::Done && !task.metadata.contains("@completed:") {
|
||||||
|
let date = Local::now().format("%Y-%m-%d").to_string();
|
||||||
|
let completed_tag = format!("@completed:{}", date);
|
||||||
|
if task.metadata.is_empty() {
|
||||||
|
task.metadata = completed_tag;
|
||||||
|
} else {
|
||||||
|
task.metadata = format!("{} {}", task.metadata, completed_tag);
|
||||||
|
}
|
||||||
|
println!("Added @completed:{}", date);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Updated [{}]: {} → {}", hash, old_status, new_status);
|
||||||
|
|
||||||
|
let path = if is_local {
|
||||||
|
dir.join(".citrine.local")
|
||||||
|
} else {
|
||||||
|
citrine_path
|
||||||
|
};
|
||||||
|
file.write(&path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
89
src/file.rs
Normal file
89
src/file.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use crate::task::{parse_line, Line, Task};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct CitrineFile {
|
||||||
|
pub lines: Vec<Line>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CitrineFile {
|
||||||
|
pub fn read(path: &Path) -> Result<Self> {
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||||
|
let lines = content.lines().map(parse_line).collect();
|
||||||
|
Ok(CitrineFile { lines })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_or_empty(path: &Path) -> Self {
|
||||||
|
Self::read(path).unwrap_or(CitrineFile { lines: vec![] })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self, path: &Path) -> Result<()> {
|
||||||
|
let content: String = self.lines.iter().map(|l| l.to_string()).collect::<Vec<_>>().join("\n");
|
||||||
|
let content = if content.is_empty() { content } else { content + "\n" };
|
||||||
|
fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tasks(&self) -> Vec<&Task> {
|
||||||
|
self.lines.iter().filter_map(|l| match l {
|
||||||
|
Line::Task(t) => Some(t),
|
||||||
|
_ => None,
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hashes(&self) -> HashSet<String> {
|
||||||
|
self.tasks().iter().map(|t| t.hash.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_task_mut(&mut self, hash: &str) -> Option<&mut Task> {
|
||||||
|
self.lines.iter_mut().find_map(|l| match l {
|
||||||
|
Line::Task(t) if t.hash == hash => Some(t),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_task(&mut self, hash: &str) -> Option<Task> {
|
||||||
|
let pos = self.lines.iter().position(|l| matches!(l, Line::Task(t) if t.hash == hash));
|
||||||
|
if let Some(i) = pos {
|
||||||
|
if let Line::Task(t) = self.lines.remove(i) {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_all_hashes(dir: &Path) -> HashSet<String> {
|
||||||
|
let mut hashes = HashSet::new();
|
||||||
|
let main_file = CitrineFile::read_or_empty(&dir.join(".citrine"));
|
||||||
|
let local_file = CitrineFile::read_or_empty(&dir.join(".citrine.local"));
|
||||||
|
hashes.extend(main_file.hashes());
|
||||||
|
hashes.extend(local_file.hashes());
|
||||||
|
hashes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_hash(main: &CitrineFile, local: &CitrineFile, partial: &str) -> Result<(String, bool)> {
|
||||||
|
let mut matches: Vec<(String, bool)> = Vec::new();
|
||||||
|
|
||||||
|
for t in main.tasks() {
|
||||||
|
if t.hash.starts_with(partial) {
|
||||||
|
matches.push((t.hash.clone(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for t in local.tasks() {
|
||||||
|
if t.hash.starts_with(partial) {
|
||||||
|
matches.push((t.hash.clone(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match matches.len() {
|
||||||
|
0 => anyhow::bail!("Task not found: {}", partial),
|
||||||
|
1 => Ok(matches.remove(0)),
|
||||||
|
_ => {
|
||||||
|
let hash_list: Vec<&str> = matches.iter().map(|(h, _)| h.as_str()).collect();
|
||||||
|
anyhow::bail!("Ambiguous hash '{}': matches {}", partial, hash_list.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/hash.rs
Normal file
35
src/hash.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use anyhow::{bail, Result};
|
||||||
|
use rand::Rng;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
pub fn generate_hash(existing: &HashSet<String>) -> Result<String> {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
for _ in 0..10 {
|
||||||
|
let hash = format!("{:04x}", rng.gen_range(0..0x10000u32));
|
||||||
|
if !existing.contains(&hash) {
|
||||||
|
return Ok(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("Failed to generate unique hash after 10 attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_hash() {
|
||||||
|
let existing = HashSet::new();
|
||||||
|
let hash = generate_hash(&existing).unwrap();
|
||||||
|
assert_eq!(hash.len(), 4);
|
||||||
|
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_collision() {
|
||||||
|
let mut existing = HashSet::new();
|
||||||
|
existing.insert("abcd".to_string());
|
||||||
|
let hash = generate_hash(&existing).unwrap();
|
||||||
|
assert_ne!(hash, "abcd");
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod task;
|
||||||
|
pub mod hash;
|
||||||
|
pub mod file;
|
||||||
|
pub mod commands;
|
||||||
84
src/main.rs
Normal file
84
src/main.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::env;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
mod task;
|
||||||
|
mod hash;
|
||||||
|
mod file;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "citrine", version, about = "Minimalist plaintext todo CLI")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Initialize citrine in current directory
|
||||||
|
Init,
|
||||||
|
/// Add a new task
|
||||||
|
Add {
|
||||||
|
/// Task description
|
||||||
|
description: String,
|
||||||
|
/// Add to .citrine.local instead
|
||||||
|
#[arg(long)]
|
||||||
|
local: bool,
|
||||||
|
},
|
||||||
|
/// List tasks
|
||||||
|
List {
|
||||||
|
/// Filter by status: todo, progress, blocked, done
|
||||||
|
#[arg(long)]
|
||||||
|
status: Option<String>,
|
||||||
|
/// Show only local tasks
|
||||||
|
#[arg(long)]
|
||||||
|
local: bool,
|
||||||
|
},
|
||||||
|
/// Set task status
|
||||||
|
Set {
|
||||||
|
/// Task hash (full or partial)
|
||||||
|
hash: String,
|
||||||
|
/// New status: todo, progress, blocked, done
|
||||||
|
status: String,
|
||||||
|
},
|
||||||
|
/// Mark task as done
|
||||||
|
Done {
|
||||||
|
/// Task hash (full or partial)
|
||||||
|
hash: String,
|
||||||
|
},
|
||||||
|
/// Remove a task
|
||||||
|
Rm {
|
||||||
|
/// Task hash (full or partial)
|
||||||
|
hash: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let dir = env::current_dir().unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = match cli.command {
|
||||||
|
Commands::Init => commands::init::run(&dir),
|
||||||
|
Commands::Add { description, local } => commands::add::run(&dir, &description, local),
|
||||||
|
Commands::List { status, local } => commands::list::run(&dir, status.as_deref(), local),
|
||||||
|
Commands::Set { hash, status } => commands::set::run(&dir, &hash, &status),
|
||||||
|
Commands::Done { hash } => commands::set::run(&dir, &hash, "done"),
|
||||||
|
Commands::Rm { hash } => commands::rm::run(&dir, &hash),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
let msg = e.to_string();
|
||||||
|
eprintln!("Error: {}", msg);
|
||||||
|
if msg.contains("Not initialized") {
|
||||||
|
process::exit(3);
|
||||||
|
} else if msg.contains("Invalid status") || msg.contains("Ambiguous hash") {
|
||||||
|
process::exit(2);
|
||||||
|
} else {
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/task.rs
Normal file
168
src/task.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Status {
|
||||||
|
Todo,
|
||||||
|
InProgress,
|
||||||
|
Blocked,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub fn symbol(&self) -> char {
|
||||||
|
match self {
|
||||||
|
Status::Todo => ' ',
|
||||||
|
Status::InProgress => '>',
|
||||||
|
Status::Blocked => '-',
|
||||||
|
Status::Done => 'x',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_symbol(c: char) -> Option<Self> {
|
||||||
|
match c {
|
||||||
|
' ' => Some(Status::Todo),
|
||||||
|
'>' => Some(Status::InProgress),
|
||||||
|
'-' => Some(Status::Blocked),
|
||||||
|
'x' => Some(Status::Done),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_name(name: &str) -> Option<Self> {
|
||||||
|
match name {
|
||||||
|
"todo" => Some(Status::Todo),
|
||||||
|
"progress" => Some(Status::InProgress),
|
||||||
|
"blocked" => Some(Status::Blocked),
|
||||||
|
"done" => Some(Status::Done),
|
||||||
|
" " => Some(Status::Todo),
|
||||||
|
">" => Some(Status::InProgress),
|
||||||
|
"-" => Some(Status::Blocked),
|
||||||
|
"x" => Some(Status::Done),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Status {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "[{}]", self.symbol())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Task {
|
||||||
|
pub hash: String,
|
||||||
|
pub status: Status,
|
||||||
|
pub description: String,
|
||||||
|
pub metadata: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Task {
|
||||||
|
pub fn to_line(&self) -> String {
|
||||||
|
let meta = if self.metadata.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" {}", self.metadata)
|
||||||
|
};
|
||||||
|
format!("[{}] {} {}{}", self.hash, self.status, self.description, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Line {
|
||||||
|
Task(Task),
|
||||||
|
Section(String),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Line::Task(t) => t.to_line(),
|
||||||
|
Line::Section(s) => format!("## {}", s),
|
||||||
|
Line::Other(s) => s.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_line(line: &str) -> Line {
|
||||||
|
let task_re = Regex::new(r"^\[([0-9a-f]{4})\]\s+\[([x\->]| )\]\s+(.+?)(?:\s+(@.*))?$").unwrap();
|
||||||
|
let section_re = Regex::new(r"^##\s+(.+)$").unwrap();
|
||||||
|
|
||||||
|
if let Some(caps) = task_re.captures(line) {
|
||||||
|
let hash = caps[1].to_string();
|
||||||
|
let status = Status::from_symbol(caps[2].chars().next().unwrap()).unwrap();
|
||||||
|
let description = caps[3].to_string();
|
||||||
|
let metadata = caps.get(4).map_or(String::new(), |m| m.as_str().to_string());
|
||||||
|
Line::Task(Task { hash, status, description, metadata })
|
||||||
|
} else if let Some(caps) = section_re.captures(line) {
|
||||||
|
Line::Section(caps[1].to_string())
|
||||||
|
} else {
|
||||||
|
Line::Other(line.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_task_line() {
|
||||||
|
let line = "[a4f9] [ ] Implement caching layer @priority:high @backend";
|
||||||
|
if let Line::Task(t) = parse_line(line) {
|
||||||
|
assert_eq!(t.hash, "a4f9");
|
||||||
|
assert_eq!(t.status, Status::Todo);
|
||||||
|
assert_eq!(t.description, "Implement caching layer");
|
||||||
|
assert_eq!(t.metadata, "@priority:high @backend");
|
||||||
|
} else {
|
||||||
|
panic!("Expected task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_done_task() {
|
||||||
|
let line = "[f1a2] [x] Set up Docker config @completed:2025-02-14";
|
||||||
|
if let Line::Task(t) = parse_line(line) {
|
||||||
|
assert_eq!(t.status, Status::Done);
|
||||||
|
} else {
|
||||||
|
panic!("Expected task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_section() {
|
||||||
|
let line = "## Backend";
|
||||||
|
if let Line::Section(s) = parse_line(line) {
|
||||||
|
assert_eq!(s, "Backend");
|
||||||
|
} else {
|
||||||
|
panic!("Expected section");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_other() {
|
||||||
|
let line = "some random text";
|
||||||
|
assert!(matches!(parse_line(line), Line::Other(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_task_roundtrip() {
|
||||||
|
let line = "[a4f9] [ ] Implement caching layer @priority:high @backend";
|
||||||
|
if let Line::Task(t) = parse_line(line) {
|
||||||
|
assert_eq!(t.to_line(), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_task_no_metadata() {
|
||||||
|
let line = "[d4b3] [ ] Build settings page";
|
||||||
|
if let Line::Task(t) = parse_line(line) {
|
||||||
|
assert_eq!(t.description, "Build settings page");
|
||||||
|
assert_eq!(t.metadata, "");
|
||||||
|
assert_eq!(t.to_line(), line);
|
||||||
|
} else {
|
||||||
|
panic!("Expected task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tests/fixtures/sample.citrine
vendored
Normal file
9
tests/fixtures/sample.citrine
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## Backend
|
||||||
|
[a4f9] [ ] Implement caching layer @priority:high @backend
|
||||||
|
[b2e1] [>] Refactor auth module @assigned:cursor
|
||||||
|
[c3d8] [-] Deploy to staging @blocked:waiting-for-credentials
|
||||||
|
[f1a2] [x] Set up Docker config @completed:2025-02-14
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
[d4b3] [ ] Build settings page
|
||||||
|
[e5c2] [>] Fix mobile layout issues @priority:medium
|
||||||
204
tests/integration.rs
Normal file
204
tests/integration.rs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn citrine_bin() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_BIN_EXE_citrine"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_citrine(dir: &std::path::Path, args: &[&str]) -> std::process::Output {
|
||||||
|
Command::new(citrine_bin())
|
||||||
|
.current_dir(dir)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to execute citrine")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stdout(output: &std::process::Output) -> String {
|
||||||
|
String::from_utf8_lossy(&output.stdout).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stderr(output: &std::process::Output) -> String {
|
||||||
|
String::from_utf8_lossy(&output.stderr).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let out = run_citrine(dir.path(), &["init"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("Initialized citrine"));
|
||||||
|
assert!(dir.path().join(".citrine").exists());
|
||||||
|
assert!(dir.path().join(".gitignore").exists());
|
||||||
|
let gitignore = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||||
|
assert!(gitignore.contains(".citrine.local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_idempotent() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["init"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_and_list() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["add", "My first task"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("Added task"));
|
||||||
|
assert!(stdout(&out).contains("My first task"));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["list"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("My first task"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_local() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Local task", "--local"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("(local)"));
|
||||||
|
assert!(dir.path().join(".citrine.local").exists());
|
||||||
|
|
||||||
|
// Should appear in list
|
||||||
|
let out = run_citrine(dir.path(), &["list"]);
|
||||||
|
assert!(stdout(&out).contains("Local task"));
|
||||||
|
|
||||||
|
// Should appear in list --local
|
||||||
|
let out = run_citrine(dir.path(), &["list", "--local"]);
|
||||||
|
assert!(stdout(&out).contains("Local task"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_not_initialized() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Some task"]);
|
||||||
|
assert!(!out.status.success());
|
||||||
|
assert_eq!(out.status.code(), Some(3));
|
||||||
|
assert!(stderr(&out).contains("Not initialized"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_status() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Test task"]);
|
||||||
|
let hash = extract_hash(&stdout(&out));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["set", &hash, "progress"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("→"));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["list"]);
|
||||||
|
assert!(stdout(&out).contains("[>]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_done_adds_completed() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Finish this"]);
|
||||||
|
let hash = extract_hash(&stdout(&out));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["done", &hash]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("@completed:"));
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(dir.path().join(".citrine")).unwrap();
|
||||||
|
assert!(content.contains("[x]"));
|
||||||
|
assert!(content.contains("@completed:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rm() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Remove me"]);
|
||||||
|
let hash = extract_hash(&stdout(&out));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["rm", &hash]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(stdout(&out).contains("Removed"));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["list"]);
|
||||||
|
assert!(!stdout(&out).contains("Remove me"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_hash() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Partial hash test"]);
|
||||||
|
let hash = extract_hash(&stdout(&out));
|
||||||
|
let partial = &hash[..2];
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["set", partial, "progress"]);
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_status() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Task"]);
|
||||||
|
let hash = extract_hash(&stdout(&out));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["set", &hash, "invalid"]);
|
||||||
|
assert!(!out.status.success());
|
||||||
|
assert_eq!(out.status.code(), Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_not_found() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
let out = run_citrine(dir.path(), &["set", "ffff", "done"]);
|
||||||
|
assert!(!out.status.success());
|
||||||
|
assert!(stderr(&out).contains("Task not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_filter() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
|
||||||
|
run_citrine(dir.path(), &["add", "Todo task"]);
|
||||||
|
let out = run_citrine(dir.path(), &["add", "Done task"]);
|
||||||
|
let hash = extract_hash(&stdout(&out));
|
||||||
|
run_citrine(dir.path(), &["done", &hash]);
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["list", "--status", "todo"]);
|
||||||
|
let s = stdout(&out);
|
||||||
|
assert!(s.contains("Todo task"));
|
||||||
|
assert!(!s.contains("Done task"));
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["list", "--status", "done"]);
|
||||||
|
let s = stdout(&out);
|
||||||
|
assert!(!s.contains("Todo task"));
|
||||||
|
assert!(s.contains("Done task"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_main_and_local() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
run_citrine(dir.path(), &["init"]);
|
||||||
|
run_citrine(dir.path(), &["add", "Main task"]);
|
||||||
|
run_citrine(dir.path(), &["add", "Local task", "--local"]);
|
||||||
|
|
||||||
|
let out = run_citrine(dir.path(), &["list"]);
|
||||||
|
let s = stdout(&out);
|
||||||
|
assert!(s.contains("Main task"));
|
||||||
|
assert!(s.contains("Local task"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_hash(output: &str) -> String {
|
||||||
|
let re = regex::Regex::new(r"\[([0-9a-f]{4})\]").unwrap();
|
||||||
|
re.captures(output).unwrap()[1].to_string()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user