diff --git a/.gitignore b/.gitignore index afff128d8d..9981a021a4 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ storybook-static # Ignore Gradle project-specific cache directory .gradle .kotlin + +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 87d6cbc720..8d05599d64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.1.0" @@ -377,6 +383,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -469,6 +481,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "command-group" version = "5.0.1" @@ -525,6 +547,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -720,6 +752,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "document-features" version = "0.2.11" @@ -850,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.58", "winapi", ] @@ -860,7 +898,7 @@ version = "0.8.3" source = "git+https://github.com/cammisuli/wezterm?rev=b538ee29e1e89eeb4832fb35ae095564dce34c29#b538ee29e1e89eeb4832fb35ae095564dce34c29" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.58", "winapi", ] @@ -1020,6 +1058,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -1096,8 +1140,8 @@ dependencies = [ "btoi", "gix-date", "itoa", - "thiserror", - "winnow", + "thiserror 1.0.58", + "winnow 0.5.40", ] [[package]] @@ -1116,9 +1160,9 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.58", "unicode-bom", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -1131,7 +1175,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1142,7 +1186,7 @@ checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c" dependencies = [ "bstr", "itoa", - "thiserror", + "thiserror 1.0.58", "time", ] @@ -1188,7 +1232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f8cf8c2266f63e582b7eb206799b63aa5fa68ee510ad349f637dfe2d0653de0" dependencies = [ "faster-hex", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1199,7 +1243,7 @@ checksum = "7e5c65e6a29830a435664891ced3f3c1af010f14900226019590ee0971a22f37" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1217,8 +1261,8 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror", - "winnow", + "thiserror 1.0.58", + "winnow 0.5.40", ] [[package]] @@ -1231,7 +1275,7 @@ dependencies = [ "gix-trace", "home", "once_cell", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1251,8 +1295,8 @@ dependencies = [ "gix-tempfile", "gix-validate", "memmap2", - "thiserror", - "winnow", + "thiserror 1.0.58", + "winnow 0.5.40", ] [[package]] @@ -1303,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545" dependencies = [ "bstr", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1336,6 +1380,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1383,12 +1446,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "home" version = "0.5.9" @@ -1447,6 +1504,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1467,6 +1525,7 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", "rustls-pki-types", "tokio", @@ -1564,7 +1623,7 @@ dependencies = [ "miette", "project-origins", "radix_trie", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", ] @@ -1583,7 +1642,7 @@ dependencies = [ "normalize-path", "project-origins", "radix_trie", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", ] @@ -1607,6 +1666,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + [[package]] name = "indoc" version = "2.0.6" @@ -1635,14 +1704,12 @@ dependencies = [ [[package]] name = "insta" -version = "1.42.2" +version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ "console", - "linked-hash-map", "once_cell", - "pin-project", "similar", ] @@ -1659,6 +1726,21 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ioctl-rs" version = "0.1.6" @@ -1710,6 +1792,28 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.58", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -1726,6 +1830,92 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-types", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "pin-project", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" +dependencies = [ + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1785,12 +1975,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1887,7 +2071,7 @@ checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ "miette-derive", "once_cell", - "thiserror", + "thiserror 1.0.58", "unicode-width 0.1.11", ] @@ -2159,16 +2343,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -2201,7 +2375,9 @@ dependencies = [ "ignore", "ignore-files 2.1.0", "insta", + "interprocess", "itertools 0.10.5", + "jsonrpsee", "machine-uid", "mio 1.0.3", "napi", @@ -2219,6 +2395,8 @@ dependencies = [ "reqwest", "rkyv", "rusqlite", + "serde", + "serde_json", "swc_common", "swc_ecma_ast", "swc_ecma_dep_graph", @@ -2228,7 +2406,7 @@ dependencies = [ "tar", "tempfile", "terminal-colorsaurus", - "thiserror", + "thiserror 1.0.58", "tokio", "tokio-util", "tracing", @@ -2236,6 +2414,7 @@ dependencies = [ "tracing-subscriber", "tui-logger", "tui-term 0.2.0 (git+https://github.com/JamesHenry/tui-term?rev=88e3b61425c97220c528ef76c188df10032a75dd)", + "uuid", "vt100-ctt", "walkdir", "watchexec", @@ -2332,6 +2511,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "overload" version = "0.1.1" @@ -2524,6 +2709,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -2592,7 +2786,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", ] @@ -2609,7 +2803,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "slab", - "thiserror", + "thiserror 1.0.58", "tinyvec", "tracing", ] @@ -2754,6 +2948,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2962,10 +3162,11 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2974,6 +3175,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2990,10 +3203,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] -name = "rustls-webpki" -version = "0.103.1" +name = "rustls-platform-verifier" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -3021,6 +3261,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3039,6 +3288,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.22" @@ -3047,18 +3319,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -3624,7 +3896,16 @@ version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.58", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -3638,6 +3919,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3709,27 +4001,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", - "mio 0.8.11", - "num_cpus", + "mio 1.0.3", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -3770,6 +4061,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.7.10", +] + [[package]] name = "tower" version = "0.5.2" @@ -3816,7 +4124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.58", "time", "tracing-subscriber", ] @@ -4029,6 +4337,9 @@ name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom 0.2.12", +] [[package]] name = "valuable" @@ -4205,7 +4516,7 @@ dependencies = [ "notify", "once_cell", "project-origins", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", "watchexec-events", @@ -4248,7 +4559,7 @@ checksum = "af0a778522cf0fc2fa8a8f1380e32893208cb2e7fd33e64de8bd81a00a2a7838" dependencies = [ "miette", "nix 0.27.1", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4276,6 +4587,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -4303,6 +4632,12 @@ dependencies = [ "rustix 0.38.38", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -4422,6 +4757,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4449,6 +4793,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4496,6 +4855,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4514,6 +4879,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4532,6 +4903,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4562,6 +4939,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4580,6 +4963,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4598,6 +4987,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4616,6 +5011,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4643,6 +5044,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/packages/nx/Cargo.toml b/packages/nx/Cargo.toml index a3461b8200..86d76b9fb7 100644 --- a/packages/nx/Cargo.toml +++ b/packages/nx/Cargo.toml @@ -12,6 +12,13 @@ opt-level = "z" strip = "none" [dependencies] +tokio = { version = "1.44.0", features = [ + "sync", + "macros", + "io-util", + "rt", + "time", +] } anyhow = "1.0.71" better-panic = "0.3.0" colored = "2" @@ -51,7 +58,6 @@ terminal-colorsaurus = "0.4.0" thiserror = "1.0.40" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -tokio = { version = "1.32.0", features = ['sync','macros','io-util','rt','time'] } tokio-util = "0.7.9" tracing-appender = "0.2" tui-logger = { version = "0.17.2", features = ["tracing-support"] } @@ -59,6 +65,8 @@ tui-term = { git = "https://github.com/JamesHenry/tui-term", rev = "88e3b61425c9 walkdir = '2.3.3' xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] } vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" } +serde = "1.0.219" +serde_json = "1.0.140" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["fileapi", "psapi", "shellapi"] } @@ -74,13 +82,22 @@ portable-pty = { git = "https://github.com/cammisuli/wezterm", rev = "b538ee29e1 ignore-files = "2.1.0" fs4 = "0.12.0" ratatui = { version = "0.29", features = ["scrolling-regions"] } -reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12.15", default-features = false, features = [ + "rustls-tls", +] } rusqlite = { version = "0.32.1", features = ["bundled", "array", "vtab"] } watchexec = "3.0.1" watchexec-events = "2.0.1" watchexec-filterer-ignore = "3.0.0" watchexec-signals = "2.1.0" machine-uid = "0.5.2" +interprocess = { version = "2.2.3", features = ["tokio"] } +jsonrpsee = { version = "0.25.1", features = [ + "client-core", + "async-client", + "macros", + "http-client", +] } [lib] crate-type = ['cdylib'] @@ -94,3 +111,4 @@ insta = "1.42.2" # This is only used for unit tests swc_ecma_dep_graph = "0.109.1" tempfile = "3.13.0" +uuid = { version = "1.0", features = ["v4"] } diff --git a/packages/nx/src/native/index.d.ts b/packages/nx/src/native/index.d.ts index f4f7888a66..94bb030766 100644 --- a/packages/nx/src/native/index.d.ts +++ b/packages/nx/src/native/index.d.ts @@ -8,7 +8,7 @@ export declare class ExternalObject { } } export declare class AppLifeCycle { - constructor(tasks: Array, initiatingTasks: Array, runMode: RunMode, pinnedTasks: Array, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string) + constructor(tasks: Array, initiatingTasks: Array, runMode: RunMode, pinnedTasks: Array, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string, workspaceRoot: string) startCommand(threadCount?: number | undefined | null): void scheduleTask(task: Task): void startTasks(tasks: Array, metadata: object): void diff --git a/packages/nx/src/native/index.js b/packages/nx/src/native/index.js index b0cdba7ce6..257054c6dc 100644 --- a/packages/nx/src/native/index.js +++ b/packages/nx/src/native/index.js @@ -62,10 +62,15 @@ const originalLoad = Module._load; // Will only be called once because the require cache takes over afterwards. Module._load = function (request, parent, isMain) { const modulePath = request; - if ( + // Check if we should use the native file cache (enabled by default) + const useNativeFileCache = process.env.NX_SKIP_NATIVE_FILE_CACHE !== 'true'; + // Check if this is an Nx native module (either from npm or local file) + const isNxNativeModule = nxPackages.has(modulePath) || - localNodeFiles.some((f) => modulePath.endsWith(f)) - ) { + localNodeFiles.some((file) => modulePath.endsWith(file)); + + // Only use the file cache for Nx native modules when caching is enabled + if (useNativeFileCache && isNxNativeModule) { const nativeLocation = require.resolve(modulePath); const fileName = basename(nativeLocation); diff --git a/packages/nx/src/native/tui/action.rs b/packages/nx/src/native/tui/action.rs index 34a2ce4ae4..12598a2207 100644 --- a/packages/nx/src/native/tui/action.rs +++ b/packages/nx/src/native/tui/action.rs @@ -35,4 +35,6 @@ pub enum Action { StartTasks(Vec), EndTasks(Vec), ToggleDebugMode, + SendConsoleMessage(String), + ConsoleMessengerAvailable(bool), } diff --git a/packages/nx/src/native/tui/app.rs b/packages/nx/src/native/tui/app.rs index 1e8b0f9104..afde4c64ca 100644 --- a/packages/nx/src/native/tui/app.rs +++ b/packages/nx/src/native/tui/app.rs @@ -23,7 +23,6 @@ use crate::native::{ tasks::types::{Task, TaskResult}, }; -use super::action::Action; use super::components::Component; use super::components::countdown_popup::CountdownPopup; use super::components::help_popup::HelpPopup; @@ -39,6 +38,7 @@ use super::pty::PtyInstance; use super::theme::THEME; use super::tui; use super::utils::normalize_newlines; +use super::{action::Action, nx_console::messaging::NxConsoleMessageConnection}; pub struct App { pub components: Vec>, @@ -68,6 +68,7 @@ pub struct App { tasks: Vec, debug_mode: bool, debug_state: TuiWidgetState, + console_messenger: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -134,6 +135,7 @@ impl App { tasks, debug_mode: false, debug_state: TuiWidgetState::default().set_default_display_level(LevelFilter::Debug), + console_messenger: None, }) } @@ -209,6 +211,10 @@ impl App { // Show countdown popup for the configured duration (making sure the help popup is not open first) pub fn end_command(&mut self) { + self.console_messenger + .as_ref() + .and_then(|c| c.end_running_tasks()); + // If the user has interacted with the app, or auto-exit is disabled, do nothing if self.user_has_interacted || !self.tui_config.auto_exit.should_exit_automatically() { return; @@ -321,6 +327,10 @@ impl App { && !(matches!(self.focus, Focus::MultipleOutput(_)) && self.is_interactive_mode()) { + self.console_messenger + .as_ref() + .and_then(|c| c.end_running_tasks()); + self.is_forced_shutdown = true; // Quit immediately self.quit_at = Some(std::time::Instant::now()); @@ -769,8 +779,25 @@ impl App { trace!("{action:?}"); } match &action { + Action::StartCommand(_) => { + self.console_messenger + .as_ref() + .and_then(|c| c.start_running_tasks()); + } + Action::Tick => { + self.console_messenger.as_ref().and_then(|messenger| { + self.components + .iter() + .find_map(|c| c.as_any().downcast_ref::()) + .and_then(|tasks_list| { + messenger.update_running_tasks(&tasks_list.tasks, &self.pty_instances) + }) + }); + } // Quit immediately - Action::Quit => self.quit_at = Some(std::time::Instant::now()), + Action::Quit => { + self.quit_at = Some(std::time::Instant::now()); + } // Cancel quitting Action::CancelQuit => { self.quit_at = None; @@ -981,6 +1008,7 @@ impl App { is_focused, has_pty, is_next_tab_target, + self.console_messenger.is_some(), ); let terminal_pane = TerminalPane::new() @@ -1008,6 +1036,13 @@ impl App { }) .ok(); } + Action::SendConsoleMessage(msg) => { + if let Some(connection) = &self.console_messenger { + connection.send_terminal_string(msg); + } else { + trace!("No console connection available"); + } + } _ => {} } @@ -1358,7 +1393,10 @@ impl App { fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { if let Focus::MultipleOutput(pane_idx) = self.focus { let terminal_pane_data = &mut self.terminal_pane_data[pane_idx]; - terminal_pane_data.handle_key_event(key) + if let Some(action) = terminal_pane_data.handle_key_event(key)? { + self.dispatch_action(action); + } + Ok(()) } else { Ok(()) } @@ -1540,4 +1578,15 @@ impl App { self.debug_state.transition(event); } } + + pub fn set_console_messenger(&mut self, messenger: NxConsoleMessageConnection) { + self.console_messenger = Some(messenger); + if self + .console_messenger + .as_ref() + .is_some_and(|c| c.is_connected()) + { + self.dispatch_action(Action::ConsoleMessengerAvailable(true)); + } + } } diff --git a/packages/nx/src/native/tui/components/help_popup.rs b/packages/nx/src/native/tui/components/help_popup.rs index acdecc07d8..439a1ed6ad 100644 --- a/packages/nx/src/native/tui/components/help_popup.rs +++ b/packages/nx/src/native/tui/components/help_popup.rs @@ -12,9 +12,11 @@ use std::any::Any; use tokio::sync::mpsc::UnboundedSender; use super::{Component, Frame}; -use crate::native::tui::action::Action; +use crate::native::tui::{action::Action, nx_console}; + use crate::native::tui::theme::THEME; +#[derive(Default)] pub struct HelpPopup { scroll_offset: usize, scrollbar_state: ScrollbarState, @@ -22,6 +24,7 @@ pub struct HelpPopup { viewport_height: usize, visible: bool, action_tx: Option>, + console_available: bool, } impl HelpPopup { @@ -33,6 +36,7 @@ impl HelpPopup { viewport_height: 0, visible: false, action_tx: None, + console_available: false, } } @@ -40,6 +44,10 @@ impl HelpPopup { self.visible = visible; } + pub fn set_console_available(&mut self, available: bool) { + self.console_available = available; + } + // Ensure the scroll state is reset to avoid recalc issues pub fn handle_resize(&mut self, _width: u16, _height: u16) { self.scroll_offset = 0; @@ -110,7 +118,7 @@ impl HelpPopup { ]) .split(popup_layout[1])[1]; - let keybindings = vec![ + let mut keybindings = vec![ // Misc ("?", "Toggle this popup"), ("q or +c", "Quit the TUI"), @@ -149,6 +157,25 @@ impl HelpPopup { ("+z", "Stop interacting with a continuous task"), ]; + if self.console_available { + // add Copilot specific keybindings for AI assistance + + keybindings.extend([ + ("", ""), + ( + "+a", + match nx_console::get_current_editor() { + nx_console::SupportedEditor::VSCode => { + "Send terminal output to Copilot so that it can assist with any issues" + } + _ => { + "Send terminal output to your AI assistant so that it can assist with any issues" + } + }, + ), + ]); + } + let mut content: Vec = vec![ // Welcome text Line::from(vec![ @@ -357,8 +384,14 @@ impl Component for HelpPopup { } fn update(&mut self, action: Action) -> Result> { - if let Action::Resize(w, h) = action { - self.handle_resize(w, h); + match action { + Action::Resize(w, h) => { + self.handle_resize(w, h); + } + Action::ConsoleMessengerAvailable(available) => { + self.set_console_available(available); + } + _ => {} } Ok(None) } diff --git a/packages/nx/src/native/tui/components/tasks_list.rs b/packages/nx/src/native/tui/components/tasks_list.rs index 4cc94fc39b..04517e4a22 100644 --- a/packages/nx/src/native/tui/components/tasks_list.rs +++ b/packages/nx/src/native/tui/components/tasks_list.rs @@ -7,6 +7,7 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Cell, Paragraph, Row, Table}, }; +use serde::{Deserialize, Serialize}; use std::{ any::Any, sync::{Arc, Mutex}, @@ -52,7 +53,7 @@ pub struct TaskItem { cache_status: String, // Public to aid with sorting utility and testing pub status: TaskStatus, - terminal_output: String, + pub terminal_output: String, pub continuous: bool, start_time: Option, // Public to aid with sorting utility and testing @@ -115,7 +116,7 @@ impl TaskItem { } #[napi] -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TaskStatus { // Explicit statuses that can come from the task runner Success, diff --git a/packages/nx/src/native/tui/components/terminal_pane.rs b/packages/nx/src/native/tui/components/terminal_pane.rs index ca7b10c7bb..fdfd742f3d 100644 --- a/packages/nx/src/native/tui/components/terminal_pane.rs +++ b/packages/nx/src/native/tui/components/terminal_pane.rs @@ -13,9 +13,9 @@ use ratatui::{ use std::{io, sync::Arc}; use tui_term::widget::PseudoTerminal; -use super::tasks_list::TaskStatus; -use crate::native::tui::pty::PtyInstance; +use crate::native::tui::components::tasks_list::TaskStatus; use crate::native::tui::theme::THEME; +use crate::native::tui::{action::Action, pty::PtyInstance}; pub struct TerminalPaneData { pub pty: Option>, @@ -34,7 +34,7 @@ impl TerminalPaneData { } } - pub fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { + pub fn handle_key_event(&mut self, key: KeyEvent) -> io::Result> { if let Some(pty) = &mut self.pty { let mut pty_mut = pty.as_ref().clone(); match key.code { @@ -42,11 +42,11 @@ impl TerminalPaneData { // If interactive, the event falls through to be forwarded to the PTY so that we can support things like interactive prompts within tasks. KeyCode::Up | KeyCode::Char('k') if !self.is_interactive => { pty_mut.scroll_up(); - return Ok(()); + return Ok(None); } KeyCode::Down | KeyCode::Char('j') if !self.is_interactive => { pty_mut.scroll_down(); - return Ok(()); + return Ok(None); } // Handle ctrl+u and ctrl+d for scrolling when not in interactive mode KeyCode::Char('u') @@ -56,7 +56,7 @@ impl TerminalPaneData { for _ in 0..12 { pty_mut.scroll_up(); } - return Ok(()); + return Ok(None); } KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) && !self.is_interactive => @@ -65,7 +65,7 @@ impl TerminalPaneData { for _ in 0..12 { pty_mut.scroll_down(); } - return Ok(()); + return Ok(None); } // Handle 'c' for copying when not in interactive mode KeyCode::Char('c') if !self.is_interactive => { @@ -81,12 +81,20 @@ impl TerminalPaneData { } } } - return Ok(()); + return Ok(None); } // Handle 'i' to enter interactive mode for in progress tasks KeyCode::Char('i') if self.can_be_interactive && !self.is_interactive => { self.set_interactive(true); - return Ok(()); + return Ok(None); + } + KeyCode::Char('a') + if key.modifiers.contains(KeyModifiers::CONTROL) && !self.is_interactive => + { + let Some(screen) = pty.get_screen() else { + return Ok(None); + }; + return Ok(Some(Action::SendConsoleMessage(screen.all_contents()))); } // Only send input to PTY if we're in interactive mode _ if self.is_interactive => match key.code { @@ -114,7 +122,7 @@ impl TerminalPaneData { _ => {} } } - Ok(()) + Ok(None) } pub fn handle_mouse_event(&mut self, event: MouseEvent) -> io::Result<()> { @@ -161,6 +169,7 @@ pub struct TerminalPaneState { pub scrollbar_state: ScrollbarState, pub has_pty: bool, pub is_next_tab_target: bool, + pub console_available: bool, } impl TerminalPaneState { @@ -171,6 +180,7 @@ impl TerminalPaneState { is_focused: bool, has_pty: bool, is_next_tab_target: bool, + console_available: bool, ) -> Self { Self { task_name, @@ -181,6 +191,7 @@ impl TerminalPaneState { scrollbar_state: ScrollbarState::default(), has_pty, is_next_tab_target, + console_available, } } } diff --git a/packages/nx/src/native/tui/lifecycle.rs b/packages/nx/src/native/tui/lifecycle.rs index 5a5d53ff7f..346d043b11 100644 --- a/packages/nx/src/native/tui/lifecycle.rs +++ b/packages/nx/src/native/tui/lifecycle.rs @@ -5,13 +5,17 @@ use parking_lot::Mutex; use std::sync::Arc; use tracing::debug; +use crate::native::logger::enable_logger; +use crate::native::tasks::types::{Task, TaskResult}; +use crate::native::{ + pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc}, + tui::nx_console::messaging::NxConsoleMessageConnection, +}; + use super::app::App; use super::components::tasks_list::TaskStatus; use super::config::{AutoExit, TuiCliArgs as RustTuiCliArgs, TuiConfig as RustTuiConfig}; use super::tui::Tui; -use crate::native::logger::enable_logger; -use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc}; -use crate::native::tasks::types::{Task, TaskResult}; #[napi(object)] #[derive(Clone)] @@ -63,6 +67,7 @@ pub enum RunMode { #[derive(Clone)] pub struct AppLifeCycle { app: Arc>, + workspace_root: Arc, } #[napi] @@ -76,6 +81,7 @@ impl AppLifeCycle { tui_cli_args: TuiCliArgs, tui_config: TuiConfig, title_text: String, + workspace_root: String, ) -> Self { // Get the target names from nx_args.targets let rust_tui_cli_args = tui_cli_args.into(); @@ -97,6 +103,7 @@ impl AppLifeCycle { ) .unwrap(), )), + workspace_root: Arc::new(workspace_root), } } @@ -207,7 +214,14 @@ impl AppLifeCycle { debug!("Initialized Components"); + let workspace_root = self.workspace_root.clone(); napi::tokio::spawn(async move { + { + // set up Console Messenger in a async context + let connection = NxConsoleMessageConnection::new(&workspace_root).await; + app_mutex.lock().set_console_messenger(connection); + } + loop { // Handle events using our Tui abstraction if let Some(event) = tui.next().await { diff --git a/packages/nx/src/native/tui/mod.rs b/packages/nx/src/native/tui/mod.rs index 876bd28441..1e3917c062 100644 --- a/packages/nx/src/native/tui/mod.rs +++ b/packages/nx/src/native/tui/mod.rs @@ -3,6 +3,7 @@ pub mod app; pub mod components; pub mod config; pub mod lifecycle; +pub mod nx_console; pub mod pty; pub mod theme; #[allow(clippy::module_inception)] diff --git a/packages/nx/src/native/tui/nx_console.rs b/packages/nx/src/native/tui/nx_console.rs new file mode 100644 index 0000000000..615232fe67 --- /dev/null +++ b/packages/nx/src/native/tui/nx_console.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + +mod ipc_transport; +pub mod messaging; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SupportedEditor { + VSCode, + Cursor, + Windsurf, + JetBrains, + Unknown, +} + +static CURRENT_EDITOR: OnceLock = OnceLock::new(); + +pub fn get_current_editor() -> &'static SupportedEditor { + CURRENT_EDITOR.get_or_init(|| detect_editor(HashMap::new())) +} + +fn detect_editor(mut env_map: HashMap) -> SupportedEditor { + let term_editor = if let Some(term) = get_env_var("TERM_PROGRAM", &mut env_map) { + let term_lower = term.to_lowercase(); + match term_lower.as_str() { + "vscode" => SupportedEditor::VSCode, + "cursor" => SupportedEditor::Cursor, + "windsurf" => SupportedEditor::Windsurf, + "jetbrains" => SupportedEditor::JetBrains, + _ => SupportedEditor::Unknown, + } + } else { + SupportedEditor::Unknown + }; + + // For JetBrains, we don't need any additional checks + if matches!(term_editor, SupportedEditor::JetBrains) { + return term_editor; + } + + if matches!(term_editor, SupportedEditor::VSCode) { + if let Some(vscode_git_var) = get_env_var("VSCODE_GIT_ASKPASS_NODE", &mut env_map) { + let vscode_git_var_lowercase = vscode_git_var.to_lowercase(); + if vscode_git_var_lowercase.contains("cursor") { + return SupportedEditor::Cursor; + } else if vscode_git_var_lowercase.contains("windsurf") { + return SupportedEditor::Windsurf; + } else { + return SupportedEditor::VSCode; + } + } else { + return term_editor; + } + } + + SupportedEditor::Unknown +} + +fn get_env_var<'a>(name: &str, env_map: &'a mut HashMap) -> Option<&'a str> { + if env_map.contains_key(name) { + return env_map.get(name).map(|s| s.as_str()); + } + + match std::env::var(name) { + Ok(val) => { + env_map.insert(name.to_string(), val); + env_map.get(name).map(|s| s.as_str()) + } + Err(_) => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn test_detect_vscode() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/vscode/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_detect_cursor() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/cursor/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Cursor); + } + + #[test] + fn test_detect_windsurf() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/windsurf/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Windsurf); + } + + #[test] + fn test_detect_jetbrains() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "jetbrains".to_string()); + assert_eq!(detect_editor(test_env), SupportedEditor::JetBrains); + } + + #[test] + fn test_term_program_unknown() { + let mut test_env = HashMap::new(); + test_env.insert( + "TERM_PROGRAM".to_string(), + "some-unknown-editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_vscode_without_askpass_confirmation() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + // No VSCODE_GIT_ASKPASS_NODE set or doesn't contain "vscode" + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_vscode_with_wrong_askpass() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "vscode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/no/matching/editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_case_insensitivity() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "VSCode".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/VSCODE/in/it".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::VSCode); + } + + #[test] + fn test_cursor_without_askpass_confirmation() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "cursor".to_string()); + // No VSCODE_GIT_ASKPASS_NODE set + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_cursor_with_wrong_askpass() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "cursor".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/no/matching/editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_windsurf_without_askpass_confirmation() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "windsurf".to_string()); + // No VSCODE_GIT_ASKPASS_NODE set + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } + + #[test] + fn test_windsurf_with_wrong_askpass() { + let mut test_env = HashMap::new(); + test_env.insert("TERM_PROGRAM".to_string(), "windsurf".to_string()); + test_env.insert( + "VSCODE_GIT_ASKPASS_NODE".to_string(), + "some/path/with/no/matching/editor".to_string(), + ); + assert_eq!(detect_editor(test_env), SupportedEditor::Unknown); + } +} diff --git a/packages/nx/src/native/tui/nx_console/ipc_transport.rs b/packages/nx/src/native/tui/nx_console/ipc_transport.rs new file mode 100644 index 0000000000..c4e187484c --- /dev/null +++ b/packages/nx/src/native/tui/nx_console/ipc_transport.rs @@ -0,0 +1,320 @@ +use std::path::Path; +use std::sync::Arc; + +use anyhow::anyhow; +use interprocess::{ + bound_util::{RefTokioAsyncRead, RefTokioAsyncWrite}, + local_socket::{ + GenericFilePath, ToFsName, + tokio::{Stream, prelude::*}, + }, +}; +use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT}; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +pub struct IpcTransport { + pub reader: IpcTransportReceiver, + pub writer: IpcTransportSender, +} +impl IpcTransport { + pub async fn new(socket_path: &Path) -> Result { + let socket_path = socket_path.to_fs_name::()?; + let conn = Stream::connect(socket_path).await?; + let stream = Arc::new(conn); + let writer = IpcTransportSender(Arc::clone(&stream)); + let reader = IpcTransportReceiver(Arc::clone(&stream)); + Ok(Self { reader, writer }) + } +} + +pub struct IpcTransportSender(Arc); +pub struct IpcTransportReceiver(Arc); + +const NEW_LINE: &str = "\r\n"; + +#[derive(Debug, Error)] +#[error(transparent)] +pub enum IpcError { + GenericError(#[from] anyhow::Error), + IoError(#[from] std::io::Error), +} + +impl TransportSenderT for IpcTransportSender { + type Error = IpcError; + + async fn send(&mut self, msg: String) -> Result<(), Self::Error> { + let mut stream = self.0.as_tokio_async_write(); + let headers = format!("content-length: {}{}{}", msg.len(), NEW_LINE, NEW_LINE); + stream.write_all(headers.as_bytes()).await?; + stream.flush().await?; + let mut msg = msg; + msg.push_str(NEW_LINE); + msg.push_str(NEW_LINE); + stream.write_all(msg.as_bytes()).await?; + stream.flush().await?; + Ok(()) + } +} + +impl TransportReceiverT for IpcTransportReceiver { + type Error = IpcError; + + async fn receive(&mut self) -> Result { + let mut stream = self.0.as_tokio_async_read(); + let mut response_data = Vec::new(); + let mut buffer = [0u8; 1024]; + + loop { + let bytes_read = stream.read(buffer.as_mut()).await?; + if bytes_read == 0 { + break; + } + response_data.extend_from_slice(&buffer[..bytes_read]); + + if let Ok(response_str) = String::from_utf8(response_data.clone()) { + if response_str.contains('\n') { + let parts: Vec<&str> = response_str.split('\n').collect(); + if let Some(response_part) = parts.first() { + return Ok(ReceivedMessage::Text(response_part.to_string())); + } + } + } + } + + Err(anyhow!("Failed to read from IPC stream").into()) + } +} + +#[cfg(test)] +mod test_utils { + use super::*; + use std::time::Duration; + use std::{future::Future, path::PathBuf}; + use tokio::task; + + // Define trait for platform-specific test setup + pub trait IpcTestSetup { + type ServerHandle: Sized; + type ServerSocket: AsyncReadExt + AsyncWriteExt + Unpin; + type AcceptFuture: Future + Send; + type ConnectFuture: Future> + Send; + + // Setup connection paths + fn create_connection_path() -> (PathBuf, PathBuf); + + // Create server + fn create_server(path: PathBuf) -> Self::ServerHandle; + + // Accept client connection + fn accept_connection(handle: &mut Self::ServerHandle) -> Self::AcceptFuture; + + // Connect client + fn connect_client(path: PathBuf) -> Self::ConnectFuture; + } + + // Common test implementations + pub async fn test_ipc_transport_connection() { + let (server_path, client_path) = T::create_connection_path(); + + // Create a mock server + let mut server = T::create_server(server_path); + + // Connect in a separate task + let client_task = task::spawn(async move { + // Small delay to ensure server is ready + tokio::time::sleep(Duration::from_millis(100)).await; + T::connect_client(client_path).await + }); + + // Accept the connection + T::accept_connection(&mut server).await; + + let result = client_task.await.unwrap(); + assert!(result.is_ok()); + } + + pub async fn test_transport_sender_send() { + let (server_path, client_path) = T::create_connection_path(); + + // Create a mock server + let mut server = T::create_server(server_path); + + // Start client in background + let client_task = task::spawn(async move { + let mut transport = T::connect_client(client_path).await.unwrap(); + transport.writer.send("test message".to_string()).await + }); + + // Accept the connection and read the message + let mut socket = T::accept_connection(&mut server).await; + let mut buf = [0u8; 1024]; + let n = socket.read(&mut buf).await.unwrap(); + let received = String::from_utf8_lossy(&buf[0..n]); + + assert!(received.contains("content-length: 12")); + + let result = client_task.await.unwrap(); + assert!(result.is_ok()); + } + + pub async fn test_transport_receiver_receive() { + let (server_path, client_path) = T::create_connection_path(); + + // Create a mock server + let mut server = T::create_server(server_path); + + // Start client in background + let client_task = task::spawn(async move { + let mut transport = T::connect_client(client_path).await.unwrap(); + transport.reader.receive().await + }); + + // Accept connection and send a message + let mut socket = T::accept_connection(&mut server).await; + let message = "test\nresponse"; + socket.write_all(message.as_bytes()).await.unwrap(); + socket.flush().await.unwrap(); + + let result = client_task.await.unwrap(); + assert!(result.is_ok()); + if let Ok(ReceivedMessage::Text(text)) = result { + assert_eq!(text, "test"); + } else { + panic!("Expected Text message"); + } + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::test_utils::*; + use super::*; + use std::pin::Pin; + use std::{future::Future, path::PathBuf}; + use tempfile::NamedTempFile; + use tokio::net::UnixListener; + + struct UnixIpcTestSetup; + + // Define concrete future types + type AcceptConnectionFuture = Pin + Send>>; + type ConnectClientFuture = + Pin> + Send>>; + + impl IpcTestSetup for UnixIpcTestSetup { + type ServerHandle = UnixListener; + type ServerSocket = tokio::net::UnixStream; + type AcceptFuture = AcceptConnectionFuture; + type ConnectFuture = ConnectClientFuture; + + fn create_connection_path() -> (PathBuf, PathBuf) { + let temp_file = NamedTempFile::new().unwrap(); + let socket_path = temp_file.path().to_path_buf(); + std::fs::remove_file(&socket_path).unwrap_or(()); + (socket_path.clone(), socket_path) + } + + fn create_server(path: PathBuf) -> Self::ServerHandle { + UnixListener::bind(&path).unwrap() + } + + fn accept_connection(handle: &mut Self::ServerHandle) -> Self::AcceptFuture { + // Take ownership of the listener and create a new one for subsequent calls + let path = std::env::temp_dir().join(format!("socket-{}", uuid::Uuid::new_v4())); + std::fs::remove_file(&path).unwrap_or(()); + let old_listener = std::mem::replace(handle, UnixListener::bind(&path).unwrap()); + + Box::pin(async move { + let (socket, _) = old_listener.accept().await.unwrap(); + socket + }) + } + + fn connect_client(path: PathBuf) -> Self::ConnectFuture { + Box::pin(async move { IpcTransport::new(&path).await }) + } + } + + #[tokio::test] + async fn test_ipc_transport_connection() { + test_utils::test_ipc_transport_connection::().await; + } + + #[tokio::test] + async fn test_transport_sender_send() { + test_utils::test_transport_sender_send::().await; + } + + #[tokio::test] + async fn test_transport_receiver_receive() { + test_utils::test_transport_receiver_receive::().await; + } +} + +#[cfg(all(test, windows))] +mod tests_windows { + use super::test_utils::*; + use super::*; + use std::pin::Pin; + use std::{future::Future, path::PathBuf}; + use tokio::net::windows::named_pipe::ServerOptions; + use uuid::Uuid; + + struct WindowsIpcTestSetup; + + // Define concrete future types + type AcceptConnectionFuture = + Pin + Send>>; + type ConnectClientFuture = + Pin> + Send>>; + + impl IpcTestSetup for WindowsIpcTestSetup { + type ServerHandle = (ServerOptions, PathBuf); + type ServerSocket = tokio::net::windows::named_pipe::NamedPipeServer; + type AcceptFuture = AcceptConnectionFuture; + type ConnectFuture = ConnectClientFuture; + + fn create_connection_path() -> (PathBuf, PathBuf) { + let pipe_name = format!(r"\\.\pipe\test-{}", Uuid::new_v4()); + (pipe_name.clone().into(), pipe_name.into()) + } + + fn create_server(path: PathBuf) -> Self::ServerHandle { + let mut options = ServerOptions::new(); + options.first_pipe_instance(true); + (options, path) + } + + fn accept_connection(handle: &mut Self::ServerHandle) -> Self::AcceptFuture { + let (options, path) = handle; + let options = options.clone(); + let path = path.clone(); + Box::pin(async move { + let path_str = path.to_str().unwrap(); + let server = options.create(path_str).unwrap(); + server.connect().await.unwrap(); + server + }) + } + + fn connect_client(path: PathBuf) -> Self::ConnectFuture { + Box::pin(async move { IpcTransport::new(&path).await }) + } + } + + #[tokio::test] + async fn test_ipc_transport_connection() { + test_utils::test_ipc_transport_connection::().await; + } + + #[tokio::test] + async fn test_transport_sender_send() { + test_utils::test_transport_sender_send::().await; + } + + #[tokio::test] + async fn test_transport_receiver_receive() { + test_utils::test_transport_receiver_receive::().await; + } +} diff --git a/packages/nx/src/native/tui/nx_console/messaging.rs b/packages/nx/src/native/tui/nx_console/messaging.rs new file mode 100644 index 0000000000..da10cffc62 --- /dev/null +++ b/packages/nx/src/native/tui/nx_console/messaging.rs @@ -0,0 +1,164 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use tokio::time::Instant; +use tracing::trace; + +use jsonrpsee::{ + async_client::{Client, ClientBuilder}, + proc_macros::rpc, +}; + +use crate::native::{ + tui::{ + components::tasks_list::{TaskItem, TaskStatus}, + nx_console::ipc_transport::IpcTransport, + pty::PtyInstance, + }, + utils::socket_path::get_full_nx_console_socket_path, +}; + +#[derive(Serialize, Deserialize)] +pub struct UpdatedRunningTask { + pub name: String, + pub status: TaskStatus, + pub output: String, + pub continuous: bool, +} + +#[rpc(client, namespace = "nx", namespace_separator = "/")] +pub trait ConsoleRpc { + #[method(name = "terminalMessage")] + fn terminal_message(&self, text: String); + + #[method(name = "updateRunningTasks")] + fn update_running_tasks(&self, process_id: u32, updates: Vec); + #[method(name = "startedRunningTasks")] + fn start_running_tasks(&self, process_id: u32); + #[method(name = "endedRunningTasks")] + fn end_running_tasks(&self, process_id: u32); +} + +pub struct NxConsoleMessageConnection { + client: Option>, +} + +static LAST_UPDATES: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); +const THROTTLE_DURATION: Duration = Duration::from_secs(2); + +/// Utility function to check if an operation should be throttled. +/// Returns true if the operation should be throttled (skipped), false if it should proceed. +fn throttled(operation_key: &'static str, throttle_duration: Duration) -> bool { + let mut last_updates = LAST_UPDATES.lock(); + let now = Instant::now(); + + match last_updates.get(operation_key) { + Some(last) if now.duration_since(*last) < throttle_duration => true, // Should throttle + _ => { + // Update the last update time + last_updates.insert(operation_key, now); + false // Should NOT throttle, operation should proceed + } + } +} + +impl NxConsoleMessageConnection { + pub async fn new(workspace_root: &str) -> Self { + let socket_path = get_full_nx_console_socket_path(workspace_root); + let client = IpcTransport::new(&socket_path) + .await + .map(|transport| { + ClientBuilder::new().build_with_tokio(transport.writer, transport.reader) + }) + .inspect_err(|e| { + trace!(?socket_path, "Could not connect to Nx Console: {}", e); + }) + .ok() + .map(Arc::new); + + Self { client } + } + + pub fn is_connected(&self) -> bool { + self.client.is_some() + } + + pub fn send_terminal_string(&self, message: impl Into) -> Option<()> { + self.client.as_ref().map(|client| { + let message = message.into(); + let client = client.clone(); + tokio::spawn(async move { + if let Err(e) = client.terminal_message(message).await { + trace!("Failed to send terminal message: {}", e); + } + }); + }) + } + + pub fn update_running_tasks( + &self, + task_statuses: &[TaskItem], + ptys: &HashMap>, + ) -> Option<()> { + if throttled("update_running_tasks", THROTTLE_DURATION) { + return None; + } + + self.client.as_ref().map(|client| { + let client = client.clone(); + + let task_statuses: Vec = task_statuses + .iter() + .map(|task| { + let output = ptys + .get(&task.name) + .and_then(|pty| pty.get_screen()) + .map(|screen| screen.all_contents()) + .unwrap_or_default(); + UpdatedRunningTask { + name: task.name.clone(), + status: task.status, + output, + continuous: task.continuous, + } + }) + .collect(); + + tokio::spawn(async move { + if let Err(e) = client + .update_running_tasks(std::process::id(), task_statuses) + .await + { + trace!("Failed to send task statuses: {}", e); + } + }); + }) + } + + pub fn start_running_tasks(&self) -> Option<()> { + self.client.as_ref().map(|client| { + let client = client.clone(); + let process_id = std::process::id(); + tokio::spawn(async move { + if let Err(e) = client.start_running_tasks(process_id).await { + trace!("Failed to send start running tasks: {}", e); + } + }); + }) + } + + pub fn end_running_tasks(&self) -> Option<()> { + self.client.as_ref().map(|client| { + let client = client.clone(); + let process_id = std::process::id(); + tokio::spawn(async move { + if let Err(e) = client.end_running_tasks(process_id).await { + trace!("Failed to send end running tasks: {}", e); + } + }); + }) + } +} diff --git a/packages/nx/src/native/utils/mod.rs b/packages/nx/src/native/utils/mod.rs index ddd46fee2f..01d97e4713 100644 --- a/packages/nx/src/native/utils/mod.rs +++ b/packages/nx/src/native/utils/mod.rs @@ -2,6 +2,7 @@ mod find_matching_projects; mod get_mod_time; mod normalize_trait; pub mod path; +pub mod socket_path; pub use find_matching_projects::*; pub use get_mod_time::*; diff --git a/packages/nx/src/native/utils/socket_path.rs b/packages/nx/src/native/utils/socket_path.rs new file mode 100644 index 0000000000..480816a297 --- /dev/null +++ b/packages/nx/src/native/utils/socket_path.rs @@ -0,0 +1,95 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use crate::native::hasher::hash; + +const DAEMON_DIR_FOR_CURRENT_WORKSPACE: &str = "./nx/workspace-data/d"; + +fn socket_dir_name(workspace_root: &str, unique_name: Option<&'static str>) -> PathBuf { + let mut hashing_string = workspace_root.to_lowercase(); + if let Some(name) = unique_name { + hashing_string.push(','); + hashing_string.push_str(name); + } + let result = hash(hashing_string.as_bytes()); + let temp_dir = std::env::temp_dir(); + temp_dir.join(result) +} + +fn get_socket_dir(workspace_root: &str, unique_name: Option<&'static str>) -> PathBuf { + let dir_path = env::var("NX_SOCKET_DIR") + .or_else(|_| env::var("NX_DAEMON_SOCKET_DIR")) + .map(PathBuf::from) + .unwrap_or_else(|_| socket_dir_name(workspace_root, unique_name)); + + let path = if cfg!(target_os = "windows") { + dir_path + } else { + match fs::create_dir_all(&dir_path) { + Ok(_) => dir_path, + Err(_) => PathBuf::from(workspace_root).join(DAEMON_DIR_FOR_CURRENT_WORKSPACE), + } + }; + + if cfg!(target_os = "windows") { + let path_str = path.to_string_lossy(); + PathBuf::from(format!(r"\\.\pipe\nx\{}", path_str)) + } else { + path + } +} + +pub fn get_full_os_socket_path(workspace_root: &str) -> PathBuf { + get_socket_dir(workspace_root, None) +} + +pub fn get_full_nx_console_socket_path(workspace_root: &str) -> PathBuf { + get_socket_dir(workspace_root, Some("nx-console")).join("nx-console.sock") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_socket_dir_name_basic() { + let root = "/tmp/test_workspace"; + let dir = socket_dir_name(root, None); + assert_eq!(dir.to_string_lossy(), "/tmp/17684150229889955837"); + assert!(dir.is_absolute()); + } + + #[test] + fn test_socket_dir_name_with_unique_name() { + let root = "/tmp/test_workspace"; + let dir = socket_dir_name(root, Some("unique")); + assert_eq!(dir.to_string_lossy(), "/tmp/10757852796479033769"); + assert!(dir.is_absolute()); + } + + #[test] + fn test_get_socket_dir_env_var() { + let root = "/tmp/test_workspace"; + let temp_dir = std::env::temp_dir().join("nx_test_socket_dir"); + unsafe { env::set_var("NX_SOCKET_DIR", &temp_dir) }; + let dir = get_socket_dir(root, None); + assert_eq!(dir.to_string_lossy(), "/tmp/nx_test_socket_dir"); + unsafe { env::remove_var("NX_SOCKET_DIR") }; + } + + #[test] + fn test_get_full_os_socket_path() { + let root = "/tmp/test_workspace"; + let path = get_full_os_socket_path(root); + assert!(path.is_absolute() || path.starts_with("./nx/workspace-data/d")); + } + + #[test] + fn test_get_full_nx_console_socket_path() { + let root = "/tmp/test_workspace"; + let path = get_full_nx_console_socket_path(root); + assert!(path.to_string_lossy().contains("nx-console.sock")); + } +} diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index b259fbddcf..6cbbe250df 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -199,7 +199,8 @@ async function getTerminalOutputLifeCycle( pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, - titleText + titleText, + workspaceRoot ); lifeCycles.unshift(appLifeCycle);