mirror of
https://github.com/rtk-ai/rtk.git
synced 2026-09-19 07:33:17 +08:00
fix: resolve Windows command wrapper execution (#269)
- add PATHEXT-aware command resolution via which\n- migrate affected command runners to resolved_command\n- add regression coverage for wrapper/fallback behavior
This commit is contained in:
@@ -38,6 +38,7 @@ benchmark-report.md
|
||||
*.sqlite3
|
||||
rtk_tracking.db
|
||||
claudedocs
|
||||
.omc
|
||||
|
||||
# Vitals provenance data
|
||||
.vitals/
|
||||
|
||||
Generated
+272
-72
@@ -90,9 +90,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -108,9 +108,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -133,15 +133,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.54"
|
||||
version = "1.2.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -155,9 +155,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
@@ -168,9 +168,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.54"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -178,9 +178,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.54"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -190,9 +190,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -202,9 +202,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.7"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
@@ -323,6 +323,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -359,9 +365,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
@@ -373,6 +379,12 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -405,14 +417,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -437,6 +450,15 @@ dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -471,9 +493,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
@@ -574,6 +596,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -619,6 +647,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -635,9 +665,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -650,18 +680,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.12"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -678,9 +713,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -696,9 +731,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
@@ -758,6 +793,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -778,18 +823,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.43"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
@@ -804,9 +849,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -816,9 +861,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -827,9 +872,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
@@ -869,6 +914,7 @@ dependencies = [
|
||||
"toml",
|
||||
"ureq",
|
||||
"walkdir",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -887,9 +933,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@@ -948,6 +994,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -1050,9 +1102,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1072,12 +1124,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
version = "3.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -1162,9 +1214,15 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
@@ -1250,10 +1308,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
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 = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1264,9 +1331,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1274,9 +1341,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1287,13 +1354,47 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
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 = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -1312,6 +1413,17 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0"
|
||||
dependencies = [
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
@@ -1539,18 +1651,106 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[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 = "writeable"
|
||||
@@ -1583,18 +1783,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.33"
|
||||
version = "0.8.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
||||
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.33"
|
||||
version = "0.8.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
|
||||
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1663,6 +1863,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.16"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -32,6 +32,7 @@ ureq = "2"
|
||||
hostname = "0.4"
|
||||
flate2 = "1.0"
|
||||
quick-xml = "0.37"
|
||||
which = "8"
|
||||
|
||||
[build-dependencies]
|
||||
toml = "0.8"
|
||||
|
||||
@@ -200,6 +200,34 @@ rtk init --show # Should show "OpenCode: plugin installed"
|
||||
|
||||
---
|
||||
|
||||
## Problem: RTK commands fail on Windows ("program not found" or "No such file")
|
||||
|
||||
### Symptom
|
||||
```
|
||||
rtk vitest --run
|
||||
# Error: program not found
|
||||
# Or: The system cannot find the file specified
|
||||
|
||||
rtk lint .
|
||||
# Error: No such file or directory
|
||||
```
|
||||
|
||||
### Root Cause
|
||||
On Windows, Node.js tools (vitest, eslint, tsc, etc.) are installed as `.CMD` or `.BAT` wrapper scripts, not as native `.exe` binaries. Rust's `std::process::Command::new("vitest")` does not honor the Windows `PATHEXT` environment variable, so it cannot find `vitest.CMD` even when it's on PATH.
|
||||
|
||||
### Solution
|
||||
Update to rtk v0.23.1+ which resolves this via the `which` crate for proper PATH+PATHEXT resolution. All 16+ command modules now use `resolved_command()` instead of `Command::new()`.
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/rtk-ai/rtk
|
||||
rtk --version # Should be 0.23.1+
|
||||
```
|
||||
|
||||
### Affected Commands
|
||||
All commands that spawn external tools: `rtk vitest`, `rtk lint`, `rtk tsc`, `rtk pnpm`, `rtk playwright`, `rtk prisma`, `rtk next`, `rtk prettier`, `rtk ruff`, `rtk pytest`, `rtk pip`, `rtk mypy`, `rtk golangci-lint`, and others.
|
||||
|
||||
---
|
||||
|
||||
## Problem: "command not found: rtk" after installation
|
||||
|
||||
### Symptom
|
||||
|
||||
+4
-5
@@ -5,10 +5,9 @@
|
||||
|
||||
use crate::json_cmd;
|
||||
use crate::tracking;
|
||||
use crate::utils::{join_with_overflow, truncate_iso_date};
|
||||
use crate::utils::{join_with_overflow, resolved_command, truncate_iso_date};
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::Value;
|
||||
use std::process::Command;
|
||||
|
||||
const MAX_ITEMS: usize = 20;
|
||||
const JSON_COMPRESS_DEPTH: usize = 4;
|
||||
@@ -62,7 +61,7 @@ fn is_structured_operation(args: &[String]) -> bool {
|
||||
fn run_generic(subcommand: &str, args: &[String], verbose: u8, full_sub: &str) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("aws");
|
||||
let mut cmd = resolved_command("aws");
|
||||
cmd.arg(subcommand);
|
||||
|
||||
let mut has_output_flag = false;
|
||||
@@ -126,7 +125,7 @@ fn run_aws_json(
|
||||
extra_args: &[String],
|
||||
verbose: u8,
|
||||
) -> Result<(String, String, std::process::ExitStatus)> {
|
||||
let mut cmd = Command::new("aws");
|
||||
let mut cmd = resolved_command("aws");
|
||||
for arg in sub_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
@@ -197,7 +196,7 @@ fn run_s3_ls(extra_args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// s3 ls doesn't support --output json, run as-is and filter text
|
||||
let mut cmd = Command::new("aws");
|
||||
let mut cmd = resolved_command("aws");
|
||||
cmd.args(["s3", "ls"]);
|
||||
for arg in extra_args {
|
||||
cmd.arg(arg);
|
||||
|
||||
+3
-4
@@ -1,9 +1,8 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -68,7 +67,7 @@ where
|
||||
{
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
let mut cmd = resolved_command("cargo");
|
||||
cmd.arg(subcommand);
|
||||
|
||||
let restored_args = restore_double_dash(args);
|
||||
@@ -965,7 +964,7 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
if verbose > 0 {
|
||||
eprintln!("cargo passthrough: {:?}", args);
|
||||
}
|
||||
let status = Command::new("cargo")
|
||||
let status = resolved_command("cargo")
|
||||
.args(args)
|
||||
.status()
|
||||
.context("Failed to run cargo")?;
|
||||
|
||||
+5
-8
@@ -4,6 +4,7 @@
|
||||
//! Claude Code API usage metrics. Handles subprocess execution, JSON parsing,
|
||||
//! and graceful degradation when ccusage is unavailable.
|
||||
|
||||
use crate::utils::{resolved_command, tool_exists};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
@@ -84,21 +85,17 @@ struct MonthlyEntry {
|
||||
|
||||
/// Check if ccusage binary exists in PATH
|
||||
fn binary_exists() -> bool {
|
||||
Command::new("which")
|
||||
.arg("ccusage")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
tool_exists("ccusage")
|
||||
}
|
||||
|
||||
/// Build the ccusage command, falling back to npx if binary not in PATH
|
||||
fn build_command() -> Option<Command> {
|
||||
if binary_exists() {
|
||||
return Some(Command::new("ccusage"));
|
||||
return Some(resolved_command("ccusage"));
|
||||
}
|
||||
|
||||
// Fallback: try npx
|
||||
let npx_check = Command::new("npx")
|
||||
let npx_check = resolved_command("npx")
|
||||
.arg("ccusage")
|
||||
.arg("--help")
|
||||
.stdout(std::process::Stdio::null())
|
||||
@@ -106,7 +103,7 @@ fn build_command() -> Option<Command> {
|
||||
.status();
|
||||
|
||||
if npx_check.map(|s| s.success()).unwrap_or(false) {
|
||||
let mut cmd = Command::new("npx");
|
||||
let mut cmd = resolved_command("npx");
|
||||
cmd.arg("ccusage");
|
||||
return Some(cmd);
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,7 +1,7 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ContainerCmd {
|
||||
@@ -27,13 +27,13 @@ pub fn run(cmd: ContainerCmd, args: &[String], verbose: u8) -> Result<()> {
|
||||
fn docker_ps(_verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let raw = Command::new("docker")
|
||||
let raw = resolved_command("docker")
|
||||
.args(["ps"])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let output = Command::new("docker")
|
||||
let output = resolved_command("docker")
|
||||
.args([
|
||||
"ps",
|
||||
"--format",
|
||||
@@ -84,13 +84,13 @@ fn docker_ps(_verbose: u8) -> Result<()> {
|
||||
fn docker_images(_verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let raw = Command::new("docker")
|
||||
let raw = resolved_command("docker")
|
||||
.args(["images"])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let output = Command::new("docker")
|
||||
let output = resolved_command("docker")
|
||||
.args(["images", "--format", "{{.Repository}}:{{.Tag}}\t{{.Size}}"])
|
||||
.output()
|
||||
.context("Failed to run docker images")?;
|
||||
@@ -160,7 +160,7 @@ fn docker_logs(args: &[String], _verbose: u8) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = Command::new("docker")
|
||||
let output = resolved_command("docker")
|
||||
.args(["logs", "--tail", "100", container])
|
||||
.output()
|
||||
.context("Failed to run docker logs")?;
|
||||
@@ -184,7 +184,7 @@ fn docker_logs(args: &[String], _verbose: u8) -> Result<()> {
|
||||
fn kubectl_pods(args: &[String], _verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("kubectl");
|
||||
let mut cmd = resolved_command("kubectl");
|
||||
cmd.args(["get", "pods", "-o", "json"]);
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
@@ -285,7 +285,7 @@ fn kubectl_pods(args: &[String], _verbose: u8) -> Result<()> {
|
||||
fn kubectl_services(args: &[String], _verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("kubectl");
|
||||
let mut cmd = resolved_command("kubectl");
|
||||
cmd.args(["get", "services", "-o", "json"]);
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
@@ -365,7 +365,7 @@ fn kubectl_logs(args: &[String], _verbose: u8) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("kubectl");
|
||||
let mut cmd = resolved_command("kubectl");
|
||||
cmd.args(["logs", "--tail", "100", pod]);
|
||||
for arg in args.iter().skip(1) {
|
||||
cmd.arg(arg);
|
||||
@@ -529,7 +529,7 @@ pub fn run_docker_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
if verbose > 0 {
|
||||
eprintln!("docker passthrough: {:?}", args);
|
||||
}
|
||||
let status = Command::new("docker")
|
||||
let status = resolved_command("docker")
|
||||
.args(args)
|
||||
.status()
|
||||
.context("Failed to run docker")?;
|
||||
@@ -551,7 +551,7 @@ pub fn run_compose_ps(verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// Raw output for token tracking
|
||||
let raw_output = Command::new("docker")
|
||||
let raw_output = resolved_command("docker")
|
||||
.args(["compose", "ps"])
|
||||
.output()
|
||||
.context("Failed to run docker compose ps")?;
|
||||
@@ -564,7 +564,7 @@ pub fn run_compose_ps(verbose: u8) -> Result<()> {
|
||||
let raw = String::from_utf8_lossy(&raw_output.stdout).to_string();
|
||||
|
||||
// Structured output for parsing (same pattern as docker_ps)
|
||||
let output = Command::new("docker")
|
||||
let output = resolved_command("docker")
|
||||
.args([
|
||||
"compose",
|
||||
"ps",
|
||||
@@ -595,7 +595,7 @@ pub fn run_compose_ps(verbose: u8) -> Result<()> {
|
||||
pub fn run_compose_logs(service: Option<&str>, verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("docker");
|
||||
let mut cmd = resolved_command("docker");
|
||||
cmd.args(["compose", "logs", "--tail", "100"]);
|
||||
if let Some(svc) = service {
|
||||
cmd.arg(svc);
|
||||
@@ -633,7 +633,7 @@ pub fn run_compose_logs(service: Option<&str>, verbose: u8) -> Result<()> {
|
||||
pub fn run_compose_build(service: Option<&str>, verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("docker");
|
||||
let mut cmd = resolved_command("docker");
|
||||
cmd.args(["compose", "build"]);
|
||||
if let Some(svc) = service {
|
||||
cmd.arg(svc);
|
||||
@@ -674,7 +674,7 @@ pub fn run_compose_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
if verbose > 0 {
|
||||
eprintln!("docker compose passthrough: {:?}", args);
|
||||
}
|
||||
let status = Command::new("docker")
|
||||
let status = resolved_command("docker")
|
||||
.arg("compose")
|
||||
.args(args)
|
||||
.status()
|
||||
@@ -699,7 +699,7 @@ pub fn run_kubectl_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
if verbose > 0 {
|
||||
eprintln!("kubectl passthrough: {:?}", args);
|
||||
}
|
||||
let status = Command::new("kubectl")
|
||||
let status = resolved_command("kubectl")
|
||||
.args(args)
|
||||
.status()
|
||||
.context("Failed to run kubectl")?;
|
||||
|
||||
+2
-3
@@ -1,12 +1,11 @@
|
||||
use crate::json_cmd;
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
let mut cmd = Command::new("curl");
|
||||
let mut cmd = resolved_command("curl");
|
||||
cmd.arg("-s"); // Silent mode (no progress bar)
|
||||
|
||||
for arg in args {
|
||||
|
||||
+4
-5
@@ -2,11 +2,10 @@ use crate::binlog;
|
||||
use crate::dotnet_format_report;
|
||||
use crate::dotnet_trx;
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -29,7 +28,7 @@ pub fn run_restore(args: &[String], verbose: u8) -> Result<()> {
|
||||
pub fn run_format(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
let (report_path, cleanup_report_path) = resolve_format_report_path(args);
|
||||
let mut cmd = Command::new("dotnet");
|
||||
let mut cmd = resolved_command("dotnet");
|
||||
cmd.env(DOTNET_CLI_UI_LANGUAGE, DOTNET_CLI_UI_LANGUAGE_VALUE);
|
||||
cmd.arg("format");
|
||||
|
||||
@@ -80,7 +79,7 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
let subcommand = args[0].to_string_lossy().to_string();
|
||||
|
||||
let mut cmd = Command::new("dotnet");
|
||||
let mut cmd = resolved_command("dotnet");
|
||||
cmd.env(DOTNET_CLI_UI_LANGUAGE, DOTNET_CLI_UI_LANGUAGE_VALUE);
|
||||
cmd.arg(&subcommand);
|
||||
for arg in &args[1..] {
|
||||
@@ -124,7 +123,7 @@ fn run_dotnet_with_binlog(subcommand: &str, args: &[String], verbose: u8) -> Res
|
||||
// For test commands, prefer user-provided results directory; otherwise create isolated one.
|
||||
let (trx_results_dir, cleanup_trx_results_dir) = resolve_trx_results_dir(subcommand, args);
|
||||
|
||||
let mut cmd = Command::new("dotnet");
|
||||
let mut cmd = resolved_command("dotnet");
|
||||
cmd.env(DOTNET_CLI_UI_LANGUAGE, DOTNET_CLI_UI_LANGUAGE_VALUE);
|
||||
cmd.arg(subcommand);
|
||||
|
||||
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
use crate::prettier_cmd;
|
||||
use crate::ruff_cmd;
|
||||
use crate::tracking;
|
||||
use crate::utils::package_manager_exec;
|
||||
use crate::utils::{package_manager_exec, resolved_command};
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Detect formatter from project files or explicit argument
|
||||
fn detect_formatter(args: &[String]) -> String {
|
||||
@@ -72,9 +71,9 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
// Build command based on formatter
|
||||
let mut cmd = match formatter.as_str() {
|
||||
"prettier" => package_manager_exec("prettier"),
|
||||
"black" | "ruff" => Command::new(formatter.as_str()),
|
||||
"black" | "ruff" => resolved_command(formatter.as_str()),
|
||||
"biome" => package_manager_exec("biome"),
|
||||
_ => Command::new(formatter.as_str()),
|
||||
_ => resolved_command(formatter.as_str()),
|
||||
};
|
||||
|
||||
// Add formatter-specific flags
|
||||
|
||||
+16
-17
@@ -5,12 +5,11 @@
|
||||
|
||||
use crate::git;
|
||||
use crate::tracking;
|
||||
use crate::utils::{ok_confirmation, truncate};
|
||||
use crate::utils::{ok_confirmation, resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::process::Command;
|
||||
|
||||
lazy_static! {
|
||||
static ref HTML_COMMENT_RE: Regex = Regex::new(r"(?s)<!--.*?-->").unwrap();
|
||||
@@ -203,7 +202,7 @@ fn run_pr(args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> {
|
||||
fn list_prs(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args([
|
||||
"pr",
|
||||
"list",
|
||||
@@ -304,7 +303,7 @@ fn view_pr(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
||||
return run_passthrough_with_extra("gh", &["pr", "view", &pr_number], &extra_args);
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args([
|
||||
"pr",
|
||||
"view",
|
||||
@@ -475,7 +474,7 @@ fn pr_checks(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<()>
|
||||
None => return Err(anyhow::anyhow!("PR number required")),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args(["pr", "checks", &pr_number]);
|
||||
for arg in &extra_args {
|
||||
cmd.arg(arg);
|
||||
@@ -558,7 +557,7 @@ fn pr_checks(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<()>
|
||||
fn pr_status(_verbose: u8, _ultra_compact: bool) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args([
|
||||
"pr",
|
||||
"status",
|
||||
@@ -614,7 +613,7 @@ fn run_issue(args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> {
|
||||
fn list_issues(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args(["issue", "list", "--json", "number,title,state,author"]);
|
||||
|
||||
for arg in args {
|
||||
@@ -686,7 +685,7 @@ fn view_issue(args: &[String], _verbose: u8) -> Result<()> {
|
||||
None => return Err(anyhow::anyhow!("Issue number required")),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args([
|
||||
"issue",
|
||||
"view",
|
||||
@@ -782,7 +781,7 @@ fn run_workflow(args: &[String], verbose: u8, ultra_compact: bool) -> Result<()>
|
||||
fn list_runs(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args([
|
||||
"run",
|
||||
"list",
|
||||
@@ -884,7 +883,7 @@ fn view_run(args: &[String], _verbose: u8) -> Result<()> {
|
||||
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args(["run", "view", &run_id]);
|
||||
for arg in &extra_args {
|
||||
cmd.arg(arg);
|
||||
@@ -960,7 +959,7 @@ fn run_repo(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<()> {
|
||||
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.arg("repo").arg("view");
|
||||
|
||||
for arg in rest_args {
|
||||
@@ -1030,7 +1029,7 @@ fn run_repo(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<()> {
|
||||
fn pr_create(args: &[String], _verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args(["pr", "create"]);
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
@@ -1068,7 +1067,7 @@ fn pr_create(args: &[String], _verbose: u8) -> Result<()> {
|
||||
fn pr_merge(args: &[String], _verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args(["pr", "merge"]);
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
@@ -1126,7 +1125,7 @@ fn pr_diff(args: &[String], _verbose: u8) -> Result<()> {
|
||||
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.args(["pr", "diff"]);
|
||||
for arg in gh_args.iter() {
|
||||
cmd.arg(arg);
|
||||
@@ -1161,7 +1160,7 @@ fn pr_action(action: &str, args: &[String], _verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
let subcmd = &args[0];
|
||||
|
||||
let mut cmd = Command::new("gh");
|
||||
let mut cmd = resolved_command("gh");
|
||||
cmd.arg("pr");
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
@@ -1221,7 +1220,7 @@ fn run_api(args: &[String], _verbose: u8) -> Result<()> {
|
||||
fn run_passthrough_with_extra(cmd: &str, base_args: &[&str], extra_args: &[String]) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut command = Command::new(cmd);
|
||||
let mut command = resolved_command(cmd);
|
||||
for arg in base_args {
|
||||
command.arg(arg);
|
||||
}
|
||||
@@ -1252,7 +1251,7 @@ fn run_passthrough_with_extra(cmd: &str, base_args: &[&str], extra_args: &[Strin
|
||||
fn run_passthrough(cmd: &str, subcommand: &str, args: &[String]) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut command = Command::new(cmd);
|
||||
let mut command = resolved_command(cmd);
|
||||
command.arg(subcommand);
|
||||
for arg in args {
|
||||
command.arg(arg);
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
@@ -22,7 +23,7 @@ pub enum GitCommand {
|
||||
/// Create a git Command with global options (e.g. -C, -c, --git-dir, --work-tree)
|
||||
/// prepended before any subcommand arguments.
|
||||
fn git_cmd(global_args: &[String]) -> Command {
|
||||
let mut cmd = Command::new("git");
|
||||
let mut cmd = resolved_command("git");
|
||||
for arg in global_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,10 +1,9 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
@@ -40,7 +39,7 @@ struct PackageResult {
|
||||
pub fn run_test(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("go");
|
||||
let mut cmd = resolved_command("go");
|
||||
cmd.arg("test");
|
||||
|
||||
// Force JSON output if not already specified
|
||||
@@ -99,7 +98,7 @@ pub fn run_test(args: &[String], verbose: u8) -> Result<()> {
|
||||
pub fn run_build(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("go");
|
||||
let mut cmd = resolved_command("go");
|
||||
cmd.arg("build");
|
||||
|
||||
for arg in args {
|
||||
@@ -152,7 +151,7 @@ pub fn run_build(args: &[String], verbose: u8) -> Result<()> {
|
||||
pub fn run_vet(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("go");
|
||||
let mut cmd = resolved_command("go");
|
||||
cmd.arg("vet");
|
||||
|
||||
for arg in args {
|
||||
@@ -210,7 +209,7 @@ pub fn run_other(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let subcommand = args[0].to_string_lossy();
|
||||
let mut cmd = Command::new("go");
|
||||
let mut cmd = resolved_command("go");
|
||||
cmd.arg(&*subcommand);
|
||||
|
||||
for arg in &args[1..] {
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Position {
|
||||
@@ -34,7 +33,7 @@ struct GolangciOutput {
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("golangci-lint");
|
||||
let mut cmd = resolved_command("golangci-lint");
|
||||
|
||||
// Force JSON output
|
||||
let has_format = args
|
||||
|
||||
+8
-4
@@ -1,8 +1,8 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(
|
||||
pattern: &str,
|
||||
@@ -23,7 +23,7 @@ pub fn run(
|
||||
// Fix: convert BRE alternation \| → | for rg (which uses PCRE-style regex)
|
||||
let rg_pattern = pattern.replace(r"\|", "|");
|
||||
|
||||
let mut rg_cmd = Command::new("rg");
|
||||
let mut rg_cmd = resolved_command("rg");
|
||||
rg_cmd.args(["-n", "--no-heading", &rg_pattern, path]);
|
||||
|
||||
if let Some(ft) = file_type {
|
||||
@@ -40,7 +40,11 @@ pub fn run(
|
||||
|
||||
let output = rg_cmd
|
||||
.output()
|
||||
.or_else(|_| Command::new("grep").args(["-rn", pattern, path]).output())
|
||||
.or_else(|_| {
|
||||
resolved_command("grep")
|
||||
.args(["-rn", pattern, path])
|
||||
.output()
|
||||
})
|
||||
.context("grep/rg failed")?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
@@ -274,7 +278,7 @@ mod tests {
|
||||
fn test_rg_always_has_line_numbers() {
|
||||
// grep_cmd::run() always passes "-n" to rg (line 24).
|
||||
// This test documents that -n is built-in, so the clap flag is safe to ignore.
|
||||
let mut cmd = std::process::Command::new("rg");
|
||||
let mut cmd = resolved_command("rg");
|
||||
cmd.args(["-n", "--no-heading", "NONEXISTENT_PATTERN_12345", "."]);
|
||||
// If rg is available, it should accept -n without error (exit 1 = no match, not error)
|
||||
if let Ok(output) = cmd.output() {
|
||||
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{ok_confirmation, strip_ansi, truncate};
|
||||
use crate::utils::{ok_confirmation, resolved_command, strip_ansi, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
lazy_static! {
|
||||
static ref EMAIL_RE: Regex =
|
||||
@@ -27,7 +26,7 @@ fn run_gt_filtered(
|
||||
) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gt");
|
||||
let mut cmd = resolved_command("gt");
|
||||
for part in subcmd {
|
||||
cmd.arg(part);
|
||||
}
|
||||
@@ -171,7 +170,7 @@ pub fn run_other(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
fn passthrough_gt(subcommand: &str, args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("gt");
|
||||
let mut cmd = resolved_command("gt");
|
||||
cmd.arg(subcommand);
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
|
||||
+3
-5
@@ -1,12 +1,10 @@
|
||||
use crate::mypy_cmd;
|
||||
use crate::ruff_cmd;
|
||||
use crate::tracking;
|
||||
use crate::utils::{package_manager_exec, truncate};
|
||||
use crate::utils::{package_manager_exec, resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct EslintMessage {
|
||||
@@ -90,10 +88,10 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
|
||||
let (linter, explicit) = detect_linter(effective_args);
|
||||
|
||||
// Python linters use Command::new() directly (they're on PATH via pip/pipx)
|
||||
// Python linters use resolved_command() directly (they're on PATH via pip/pipx)
|
||||
// JS linters use package_manager_exec (npx/pnpm exec)
|
||||
let mut cmd = if is_python_linter(linter) {
|
||||
Command::new(linter)
|
||||
resolved_command(linter)
|
||||
} else {
|
||||
package_manager_exec(linter)
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
/// Noise directories commonly excluded from LLM context
|
||||
const NOISE_DIRS: &[&str] = &[
|
||||
@@ -51,7 +51,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
|
||||
// Build ls -la + any extra flags the user passed (e.g. -R)
|
||||
// Strip -l, -a, -h (we handle all of these ourselves)
|
||||
let mut cmd = Command::new("ls");
|
||||
let mut cmd = resolved_command("ls");
|
||||
cmd.arg("-la");
|
||||
for flag in &flags {
|
||||
if flag.starts_with("--") {
|
||||
|
||||
+6
-6
@@ -1044,7 +1044,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<()> {
|
||||
|
||||
if let Some(filter) = toml_match {
|
||||
// TOML match: capture stdout for filtering
|
||||
let result = std::process::Command::new(&args[0])
|
||||
let result = utils::resolved_command(&args[0])
|
||||
.args(&args[1..])
|
||||
.stdin(std::process::Stdio::inherit())
|
||||
.stdout(std::process::Stdio::piped()) // capture
|
||||
@@ -1089,7 +1089,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
// No TOML match: original passthrough behaviour (Stdio::inherit, streaming)
|
||||
let status = std::process::Command::new(&args[0])
|
||||
let status = utils::resolved_command(&args[0])
|
||||
.args(&args[1..])
|
||||
.stdin(std::process::Stdio::inherit())
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
@@ -1848,7 +1848,7 @@ fn main() -> Result<()> {
|
||||
_ => {
|
||||
// Passthrough other prisma subcommands
|
||||
let timer = tracking::TimedExecution::start();
|
||||
let mut cmd = std::process::Command::new("npx");
|
||||
let mut cmd = utils::resolved_command("npx");
|
||||
for arg in &args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
@@ -1865,7 +1865,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
let status = std::process::Command::new("npx")
|
||||
let status = utils::resolved_command("npx")
|
||||
.arg("prisma")
|
||||
.status()
|
||||
.context("Failed to run npx prisma")?;
|
||||
@@ -1961,7 +1961,7 @@ fn main() -> Result<()> {
|
||||
|
||||
Commands::Proxy { args } => {
|
||||
use std::io::{Read, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::Stdio;
|
||||
use std::thread;
|
||||
|
||||
if args.is_empty() {
|
||||
@@ -1997,7 +1997,7 @@ fn main() -> Result<()> {
|
||||
eprintln!("Proxy mode: {} {}", cmd_name, cmd_args.join(" "));
|
||||
}
|
||||
|
||||
let mut child = Command::new(&cmd_name)
|
||||
let mut child = utils::resolved_command(cmd_name.as_ref())
|
||||
.args(&cmd_args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
||||
+4
-16
@@ -1,17 +1,16 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{strip_ansi, truncate};
|
||||
use crate::utils::{resolved_command, strip_ansi, tool_exists, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = if which_command("mypy").is_some() {
|
||||
Command::new("mypy")
|
||||
let mut cmd = if tool_exists("mypy") {
|
||||
resolved_command("mypy")
|
||||
} else {
|
||||
let mut c = Command::new("python3");
|
||||
let mut c = resolved_command("python3");
|
||||
c.arg("-m").arg("mypy");
|
||||
c
|
||||
};
|
||||
@@ -47,17 +46,6 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
std::process::exit(output.status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
fn which_command(cmd: &str) -> Option<String> {
|
||||
Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
struct MypyError {
|
||||
file: String,
|
||||
line: usize,
|
||||
|
||||
+4
-9
@@ -1,23 +1,18 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{strip_ansi, truncate};
|
||||
use crate::utils::{resolved_command, strip_ansi, tool_exists, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// Try next directly first, fallback to npx if not found
|
||||
let next_exists = Command::new("which")
|
||||
.arg("next")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
let next_exists = tool_exists("next");
|
||||
|
||||
let mut cmd = if next_exists {
|
||||
Command::new("next")
|
||||
resolved_command("next")
|
||||
} else {
|
||||
let mut c = Command::new("npx");
|
||||
let mut c = resolved_command("npx");
|
||||
c.arg("next");
|
||||
c
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
/// Known npm subcommands that should NOT get "run" injected.
|
||||
/// Shared between production code and tests to avoid drift.
|
||||
@@ -74,7 +74,7 @@ const NPM_SUBCOMMANDS: &[&str] = &[
|
||||
pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("npm");
|
||||
let mut cmd = resolved_command("npm");
|
||||
|
||||
// Determine if this is "npm run <script>" or another npm subcommand (install, list, etc.)
|
||||
// Only inject "run" when args look like a script name, not a known npm subcommand.
|
||||
|
||||
+5
-17
@@ -1,7 +1,7 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{resolved_command, tool_exists};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Package {
|
||||
@@ -15,7 +15,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// Auto-detect uv vs pip
|
||||
let use_uv = which_command("uv").is_some();
|
||||
let use_uv = tool_exists("uv");
|
||||
let base_cmd = if use_uv { "uv" } else { "pip" };
|
||||
|
||||
if verbose > 0 && use_uv {
|
||||
@@ -51,7 +51,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
}
|
||||
|
||||
fn run_list(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, String)> {
|
||||
let mut cmd = Command::new(base_cmd);
|
||||
let mut cmd = resolved_command(base_cmd);
|
||||
|
||||
if base_cmd == "uv" {
|
||||
cmd.arg("pip");
|
||||
@@ -86,7 +86,7 @@ fn run_list(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, Str
|
||||
}
|
||||
|
||||
fn run_outdated(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, String)> {
|
||||
let mut cmd = Command::new(base_cmd);
|
||||
let mut cmd = resolved_command(base_cmd);
|
||||
|
||||
if base_cmd == "uv" {
|
||||
cmd.arg("pip");
|
||||
@@ -121,7 +121,7 @@ fn run_outdated(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String,
|
||||
}
|
||||
|
||||
fn run_passthrough(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, String)> {
|
||||
let mut cmd = Command::new(base_cmd);
|
||||
let mut cmd = resolved_command(base_cmd);
|
||||
|
||||
if base_cmd == "uv" {
|
||||
cmd.arg("pip");
|
||||
@@ -153,18 +153,6 @@ fn run_passthrough(base_cmd: &str, args: &[String], verbose: u8) -> Result<(Stri
|
||||
Ok((raw.clone(), raw))
|
||||
}
|
||||
|
||||
/// Check if a command exists in PATH
|
||||
fn which_command(cmd: &str) -> Option<String> {
|
||||
Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Filter pip list JSON output
|
||||
fn filter_pip_list(output: &str) -> String {
|
||||
let packages: Vec<Package> = match serde_json::from_str(output) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{detect_package_manager, strip_ansi};
|
||||
use crate::utils::{detect_package_manager, resolved_command, strip_ansi};
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
@@ -246,17 +246,17 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let pm = detect_package_manager();
|
||||
let mut cmd = match pm {
|
||||
"pnpm" => {
|
||||
let mut c = std::process::Command::new("pnpm");
|
||||
let mut c = resolved_command("pnpm");
|
||||
c.arg("exec").arg("--").arg("playwright");
|
||||
c
|
||||
}
|
||||
"yarn" => {
|
||||
let mut c = std::process::Command::new("yarn");
|
||||
let mut c = resolved_command("yarn");
|
||||
c.arg("exec").arg("--").arg("playwright");
|
||||
c
|
||||
}
|
||||
_ => {
|
||||
let mut c = std::process::Command::new("npx");
|
||||
let mut c = resolved_command("npx");
|
||||
c.arg("--no-install").arg("--").arg("playwright");
|
||||
c
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::parser::{
|
||||
emit_degradation_warning, emit_passthrough_warning, truncate_output, Dependency,
|
||||
@@ -294,7 +294,7 @@ pub fn run(cmd: PnpmCommand, args: &[String], verbose: u8) -> Result<()> {
|
||||
fn run_list(depth: usize, args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("pnpm");
|
||||
let mut cmd = resolved_command("pnpm");
|
||||
cmd.arg("list");
|
||||
cmd.arg(format!("--depth={}", depth));
|
||||
cmd.arg("--json");
|
||||
@@ -350,7 +350,7 @@ fn run_list(depth: usize, args: &[String], verbose: u8) -> Result<()> {
|
||||
fn run_outdated(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("pnpm");
|
||||
let mut cmd = resolved_command("pnpm");
|
||||
cmd.arg("outdated");
|
||||
cmd.arg("--format");
|
||||
cmd.arg("json");
|
||||
@@ -411,7 +411,7 @@ fn run_install(packages: &[String], args: &[String], verbose: u8) -> Result<()>
|
||||
}
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("pnpm");
|
||||
let mut cmd = resolved_command("pnpm");
|
||||
cmd.arg("install");
|
||||
|
||||
for pkg in packages {
|
||||
@@ -495,7 +495,7 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
||||
if verbose > 0 {
|
||||
eprintln!("pnpm passthrough: {:?}", args);
|
||||
}
|
||||
let status = Command::new("pnpm")
|
||||
let status = resolved_command("pnpm")
|
||||
.args(args)
|
||||
.status()
|
||||
.context("Failed to run pnpm")?;
|
||||
|
||||
+4
-9
@@ -1,4 +1,5 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::{resolved_command, tool_exists};
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -26,16 +27,10 @@ pub fn run(cmd: PrismaCommand, args: &[String], verbose: u8) -> Result<()> {
|
||||
|
||||
/// Create a Command that will run prisma (tries global first, then npx)
|
||||
fn create_prisma_command() -> Command {
|
||||
let prisma_exists = Command::new("which")
|
||||
.arg("prisma")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if prisma_exists {
|
||||
Command::new("prisma")
|
||||
if tool_exists("prisma") {
|
||||
resolved_command("prisma")
|
||||
} else {
|
||||
let mut c = Command::new("npx");
|
||||
let mut c = resolved_command("npx");
|
||||
c.arg("prisma");
|
||||
c
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@
|
||||
//! and produces compact tab-separated or key=value output.
|
||||
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
@@ -21,7 +22,7 @@ lazy_static! {
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = std::process::Command::new("psql");
|
||||
let mut cmd = resolved_command("psql");
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
+4
-17
@@ -1,7 +1,6 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, tool_exists, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ParseState {
|
||||
@@ -15,11 +14,11 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// Try to detect pytest command (could be "pytest", "python -m pytest", etc.)
|
||||
let mut cmd = if which_command("pytest").is_some() {
|
||||
Command::new("pytest")
|
||||
let mut cmd = if tool_exists("pytest") {
|
||||
resolved_command("pytest")
|
||||
} else {
|
||||
// Fallback to python -m pytest
|
||||
let mut c = Command::new("python");
|
||||
let mut c = resolved_command("python");
|
||||
c.arg("-m").arg("pytest");
|
||||
c
|
||||
};
|
||||
@@ -83,18 +82,6 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a command exists in PATH
|
||||
fn which_command(cmd: &str) -> Option<String> {
|
||||
Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Parse pytest output using state machine
|
||||
fn filter_pytest_output(output: &str) -> String {
|
||||
let mut state = ParseState::Header;
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RuffLocation {
|
||||
@@ -38,7 +37,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
|
||||
let is_format = args.iter().any(|a| a == "format");
|
||||
|
||||
let mut cmd = Command::new("ruff");
|
||||
let mut cmd = resolved_command("ruff");
|
||||
|
||||
if is_check {
|
||||
// Force JSON output for check command
|
||||
|
||||
+3
-4
@@ -7,8 +7,8 @@
|
||||
//! unless -a flag is present (respecting user intent).
|
||||
|
||||
use crate::tracking;
|
||||
use crate::utils::{resolved_command, tool_exists};
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
/// Noise directories commonly excluded from LLM context
|
||||
const NOISE_DIRS: &[&str] = &[
|
||||
@@ -44,8 +44,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// Check if tree is installed
|
||||
let tree_check = Command::new("which").arg("tree").output();
|
||||
if tree_check.is_err() || !tree_check.unwrap().status.success() {
|
||||
if !tool_exists("tree") {
|
||||
anyhow::bail!(
|
||||
"tree command not found. Install it first:\n\
|
||||
- macOS: brew install tree\n\
|
||||
@@ -55,7 +54,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("tree");
|
||||
let mut cmd = resolved_command("tree");
|
||||
|
||||
// Determine if user wants all files or default behavior
|
||||
let show_all = args.iter().any(|a| a == "-a" || a == "--all");
|
||||
|
||||
+4
-9
@@ -1,24 +1,19 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::truncate;
|
||||
use crate::utils::{resolved_command, tool_exists, truncate};
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
// Try tsc directly first, fallback to npx if not found
|
||||
let tsc_exists = Command::new("which")
|
||||
.arg("tsc")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
let tsc_exists = tool_exists("tsc");
|
||||
|
||||
let mut cmd = if tsc_exists {
|
||||
Command::new("tsc")
|
||||
resolved_command("tsc")
|
||||
} else {
|
||||
let mut c = Command::new("npx");
|
||||
let mut c = resolved_command("npx");
|
||||
c.arg("tsc");
|
||||
c
|
||||
};
|
||||
|
||||
+298
-16
@@ -7,6 +7,7 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// Truncates a string to `max_len` characters, appending `...` if needed.
|
||||
@@ -33,7 +34,7 @@ pub fn truncate(s: &str, max_len: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Strips ANSI escape codes (colors, styles) from a string.
|
||||
/// Strip ANSI escape codes (colors, styles) from a string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `text` - Text potentially containing ANSI escape codes
|
||||
@@ -54,7 +55,7 @@ pub fn strip_ansi(text: &str) -> String {
|
||||
/// Executes a command and returns cleaned stdout/stderr.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cmd` - Command to execute (e.g. "eslint")
|
||||
/// * `cmd` - Command to execute (e.g., "eslint")
|
||||
/// * `args` - Command arguments
|
||||
///
|
||||
/// # Returns
|
||||
@@ -68,7 +69,7 @@ pub fn strip_ansi(text: &str) -> String {
|
||||
/// ```
|
||||
#[allow(dead_code)]
|
||||
pub fn execute_command(cmd: &str, args: &[&str]) -> Result<(String, String, i32)> {
|
||||
let output = Command::new(cmd)
|
||||
let output = resolved_command(cmd)
|
||||
.args(args)
|
||||
.output()
|
||||
.context(format!("Failed to execute {}", cmd))?;
|
||||
@@ -83,10 +84,10 @@ pub fn execute_command(cmd: &str, args: &[&str]) -> Result<(String, String, i32)
|
||||
/// Formats a token count with K/M suffixes for readability.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `n` - Token count
|
||||
/// * `n` - Number of tokens
|
||||
///
|
||||
/// # Returns
|
||||
/// Formatted string (e.g. "1.2M", "59.2K", "694")
|
||||
/// Formatted string (e.g., "1.2M", "59.2K", "694")
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -229,29 +230,23 @@ pub fn detect_package_manager() -> &'static str {
|
||||
/// Build a Command using the detected package manager's exec mechanism.
|
||||
/// Returns a Command ready to have tool-specific args appended.
|
||||
pub fn package_manager_exec(tool: &str) -> Command {
|
||||
let tool_exists = Command::new("which")
|
||||
.arg(tool)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if tool_exists {
|
||||
Command::new(tool)
|
||||
if tool_exists(tool) {
|
||||
resolved_command(tool)
|
||||
} else {
|
||||
let pm = detect_package_manager();
|
||||
match pm {
|
||||
"pnpm" => {
|
||||
let mut c = Command::new("pnpm");
|
||||
let mut c = resolved_command("pnpm");
|
||||
c.arg("exec").arg("--").arg(tool);
|
||||
c
|
||||
}
|
||||
"yarn" => {
|
||||
let mut c = Command::new("yarn");
|
||||
let mut c = resolved_command("yarn");
|
||||
c.arg("exec").arg("--").arg(tool);
|
||||
c
|
||||
}
|
||||
_ => {
|
||||
let mut c = Command::new("npx");
|
||||
let mut c = resolved_command("npx");
|
||||
c.arg("--no-install").arg("--").arg(tool);
|
||||
c
|
||||
}
|
||||
@@ -259,6 +254,68 @@ pub fn package_manager_exec(tool: &str) -> Command {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a binary name to its full path, honoring PATHEXT on Windows.
|
||||
///
|
||||
/// On Windows, Node.js tools are installed as `.CMD`/`.BAT`/`.PS1` shims.
|
||||
/// Rust's `std::process::Command::new()` does NOT honor PATHEXT, so
|
||||
/// `Command::new("vitest")` fails even when `vitest.CMD` is on PATH.
|
||||
///
|
||||
/// This function uses the `which` crate to perform proper PATH+PATHEXT resolution.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - Binary name (e.g., "vitest", "eslint", "tsc")
|
||||
///
|
||||
/// # Returns
|
||||
/// Full path to the resolved binary, or error if not found.
|
||||
pub fn resolve_binary(name: &str) -> Result<PathBuf> {
|
||||
which::which(name).context(format!("Binary '{}' not found on PATH", name))
|
||||
}
|
||||
|
||||
/// Create a `Command` with PATHEXT-aware binary resolution.
|
||||
///
|
||||
/// Drop-in replacement for `Command::new(name)` that works on Windows
|
||||
/// with `.CMD`/`.BAT`/`.PS1` wrappers.
|
||||
///
|
||||
/// Falls back to `Command::new(name)` if resolution fails, so native
|
||||
/// commands (git, cargo) still work even if `which` can't find them.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - Binary name (e.g., "vitest", "eslint")
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Command` configured with the resolved binary path.
|
||||
pub fn resolved_command(name: &str) -> Command {
|
||||
match resolve_binary(name) {
|
||||
Ok(path) => Command::new(path),
|
||||
Err(e) => {
|
||||
// On Windows, resolution failure likely means a .CMD/.BAT wrapper
|
||||
// wasn't found — always warn so users have a signal.
|
||||
// On Unix, this is less common; only log in debug builds.
|
||||
#[cfg(target_os = "windows")]
|
||||
eprintln!(
|
||||
"rtk: Failed to resolve '{}' via PATH, falling back to direct exec: {}",
|
||||
name, e
|
||||
);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"rtk: Failed to resolve '{}' via PATH, falling back to direct exec: {}",
|
||||
name, e
|
||||
);
|
||||
}
|
||||
Command::new(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a tool exists on PATH (PATHEXT-aware on Windows).
|
||||
///
|
||||
/// Replaces manual `Command::new("which").arg(tool)` checks that fail on Windows.
|
||||
pub fn tool_exists(name: &str) -> bool {
|
||||
which::which(name).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -429,4 +486,229 @@ mod tests {
|
||||
let result = truncate(cjk, 6);
|
||||
assert!(result.ends_with("..."));
|
||||
}
|
||||
|
||||
// ===== resolve_binary tests (issue #212) =====
|
||||
|
||||
#[test]
|
||||
fn test_resolve_binary_finds_known_command() {
|
||||
// "cargo" must be on PATH in any Rust dev environment
|
||||
let result = resolve_binary("cargo");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"resolve_binary('cargo') should succeed, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_binary_returns_absolute_path() {
|
||||
let path = resolve_binary("cargo").expect("cargo should be resolvable");
|
||||
assert!(
|
||||
path.is_absolute(),
|
||||
"resolve_binary should return absolute path, got: {:?}",
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_binary_fails_for_unknown() {
|
||||
let result = resolve_binary("nonexistent_binary_xyz_99999");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"resolve_binary should fail for nonexistent binary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_binary_path_contains_binary_name() {
|
||||
let path = resolve_binary("cargo").expect("cargo should be resolvable");
|
||||
let filename = path
|
||||
.file_name()
|
||||
.expect("should have filename")
|
||||
.to_string_lossy();
|
||||
// On Windows this could be "cargo.exe", on Unix just "cargo"
|
||||
assert!(
|
||||
filename.starts_with("cargo"),
|
||||
"resolved path filename should start with 'cargo', got: {}",
|
||||
filename
|
||||
);
|
||||
}
|
||||
|
||||
// ===== resolved_command tests (issue #212) =====
|
||||
|
||||
#[test]
|
||||
fn test_resolved_command_executes_known_command() {
|
||||
let output = resolved_command("cargo")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.expect("resolved_command('cargo') should execute");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"cargo --version should succeed via resolved_command"
|
||||
);
|
||||
}
|
||||
|
||||
// ===== tool_exists tests (issue #212) =====
|
||||
|
||||
#[test]
|
||||
fn test_tool_exists_finds_cargo() {
|
||||
assert!(
|
||||
tool_exists("cargo"),
|
||||
"tool_exists('cargo') should return true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_exists_rejects_unknown() {
|
||||
assert!(
|
||||
!tool_exists("nonexistent_binary_xyz_99999"),
|
||||
"tool_exists should return false for nonexistent binary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_exists_finds_git() {
|
||||
assert!(tool_exists("git"), "tool_exists('git') should return true");
|
||||
}
|
||||
|
||||
// ===== Windows-specific PATHEXT resolution tests (issue #212) =====
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_tests {
|
||||
use super::super::*;
|
||||
use std::fs;
|
||||
|
||||
/// Create a temporary .cmd wrapper to simulate Node.js tool installation
|
||||
fn create_temp_cmd_wrapper(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
|
||||
let cmd_path = dir.join(format!("{}.cmd", name));
|
||||
fs::write(&cmd_path, "@echo off\r\necho fake-tool-output\r\n")
|
||||
.expect("failed to create .cmd wrapper");
|
||||
cmd_path
|
||||
}
|
||||
|
||||
/// Build a PATH string that includes the temp dir
|
||||
fn path_with_dir(dir: &std::path::Path) -> std::ffi::OsString {
|
||||
let original = std::env::var_os("PATH").unwrap_or_default();
|
||||
let mut new_path = std::ffi::OsString::from(dir.as_os_str());
|
||||
new_path.push(";");
|
||||
new_path.push(&original);
|
||||
new_path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_binary_finds_cmd_wrapper() {
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
create_temp_cmd_wrapper(temp_dir.path(), "fake-tool-test");
|
||||
|
||||
// Use which::which_in to avoid mutating global PATH (thread-safe)
|
||||
let search_path = path_with_dir(temp_dir.path());
|
||||
let result = which::which_in(
|
||||
"fake-tool-test",
|
||||
Some(search_path),
|
||||
std::env::current_dir().unwrap(),
|
||||
);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"which_in should find .cmd wrapper on Windows, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let path = result.unwrap();
|
||||
let ext = path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase();
|
||||
assert!(
|
||||
ext == "cmd" || ext == "bat",
|
||||
"resolved path should have .cmd/.bat extension, got: {:?}",
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_binary_finds_bat_wrapper() {
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
let bat_path = temp_dir.path().join("fake-bat-tool.bat");
|
||||
fs::write(&bat_path, "@echo off\r\necho bat-output\r\n")
|
||||
.expect("failed to create .bat wrapper");
|
||||
|
||||
let search_path = path_with_dir(temp_dir.path());
|
||||
let result = which::which_in(
|
||||
"fake-bat-tool",
|
||||
Some(search_path),
|
||||
std::env::current_dir().unwrap(),
|
||||
);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"which_in should find .bat wrapper on Windows, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolved_command_executes_cmd_wrapper() {
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
create_temp_cmd_wrapper(temp_dir.path(), "fake-exec-test");
|
||||
|
||||
// Resolve the full path, then execute it directly (no PATH mutation)
|
||||
let search_path = path_with_dir(temp_dir.path());
|
||||
let resolved = which::which_in(
|
||||
"fake-exec-test",
|
||||
Some(search_path),
|
||||
std::env::current_dir().unwrap(),
|
||||
)
|
||||
.expect("should resolve fake-exec-test");
|
||||
|
||||
let output = Command::new(&resolved).output();
|
||||
|
||||
assert!(
|
||||
output.is_ok(),
|
||||
"Command with resolved path should execute .cmd wrapper on Windows"
|
||||
);
|
||||
let output = output.unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("fake-tool-output"),
|
||||
"should get output from .cmd wrapper, got: {}",
|
||||
stdout
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolved_command_fallback_on_unknown_binary() {
|
||||
// When resolve_binary fails, resolved_command should fall back to
|
||||
// Command::new(name) instead of panicking. On Windows this also
|
||||
// prints a warning to stderr.
|
||||
let mut cmd = resolved_command("nonexistent_binary_xyz_99999");
|
||||
// The Command should be created (not panic). Attempting to run it
|
||||
// will fail, but that's expected — we just verify the fallback path
|
||||
// produces a usable Command.
|
||||
let result = cmd.output();
|
||||
assert!(
|
||||
result.is_err() || !result.unwrap().status.success(),
|
||||
"nonexistent binary should fail to execute, but resolved_command must not panic"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_exists_finds_cmd_wrapper() {
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
create_temp_cmd_wrapper(temp_dir.path(), "fake-exists-test");
|
||||
|
||||
let search_path = path_with_dir(temp_dir.path());
|
||||
let result = which::which_in(
|
||||
"fake-exists-test",
|
||||
Some(search_path),
|
||||
std::env::current_dir().unwrap(),
|
||||
);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"which_in should find .cmd wrapper on Windows"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@
|
||||
/// - `wc -c file.py` → `978`
|
||||
/// - `wc -l *.py` → table with common path prefix stripped
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
||||
let timer = tracking::TimedExecution::start();
|
||||
|
||||
let mut cmd = Command::new("wc");
|
||||
let mut cmd = resolved_command("wc");
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::tracking;
|
||||
use crate::utils::resolved_command;
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
|
||||
/// Compact wget - strips progress bars, shows only result
|
||||
pub fn run(url: &str, args: &[String], verbose: u8) -> Result<()> {
|
||||
@@ -19,7 +19,7 @@ pub fn run(url: &str, args: &[String], verbose: u8) -> Result<()> {
|
||||
}
|
||||
cmd_args.push(url);
|
||||
|
||||
let output = Command::new("wget")
|
||||
let output = resolved_command("wget")
|
||||
.args(&cmd_args)
|
||||
.output()
|
||||
.context("Failed to run wget")?;
|
||||
@@ -64,7 +64,7 @@ pub fn run_stdout(url: &str, args: &[String], verbose: u8) -> Result<()> {
|
||||
}
|
||||
cmd_args.push(url);
|
||||
|
||||
let output = Command::new("wget")
|
||||
let output = resolved_command("wget")
|
||||
.args(&cmd_args)
|
||||
.output()
|
||||
.context("Failed to run wget")?;
|
||||
|
||||
Reference in New Issue
Block a user