├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── package.json ├── renovate.json ├── src ├── args.rs ├── install.rs ├── main.rs ├── prompt.rs └── run.rs └── stuff └── example-script.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | branches: ["main"] 7 | pull_request: 8 | branches: ["main"] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - target: x86_64-unknown-linux-musl 23 | os: ubuntu-latest 24 | output: dum 25 | archive: tar.gz 26 | - target: arm-unknown-linux-musleabihf 27 | os: ubuntu-latest 28 | output: dum 29 | archive: tar.gz 30 | - target: aarch64-unknown-linux-musl 31 | os: ubuntu-latest 32 | output: dum 33 | archive: tar.gz 34 | - target: x86_64-apple-darwin 35 | os: macos-latest 36 | output: dum 37 | archive: zip 38 | - target: aarch64-apple-darwin 39 | os: macos-latest 40 | output: dum 41 | archive: zip 42 | # Failing 43 | # - target: arm-unknown-linux-musleabihf 44 | # os: ubuntu-latest 45 | # output: dum 46 | # archive: tgz 47 | - target: x86_64-pc-windows-msvc 48 | os: windows-latest 49 | output: dum.exe 50 | archive: zip 51 | - target: aarch64-pc-windows-msvc 52 | os: windows-latest 53 | output: dum.exe 54 | archive: zip 55 | 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: FranzDiebold/github-env-vars-action@v2.3.1 59 | 60 | - name: Install coreutils for macOS 61 | if: matrix.os == 'macos-latest' 62 | run: brew install coreutils 63 | 64 | - name: Setup Rust 65 | uses: actions-rust-lang/setup-rust-toolchain@v1 66 | with: 67 | target: ${{ matrix.target }} 68 | 69 | - name: Install Cross 70 | if: matrix.os == 'ubuntu-latest' 71 | run: cargo install cross 72 | 73 | - name: Run Tests 74 | run: cargo test 75 | 76 | - name: Build release (non-Linux) 77 | if: matrix.os != 'ubuntu-latest' 78 | run: cargo build --target ${{ matrix.target }} --release 79 | 80 | - name: Build release (Linux) 81 | if: matrix.os == 'ubuntu-latest' 82 | run: cross build --target ${{ matrix.target }} --release 83 | 84 | - name: Copy and rename utility 85 | run: cp target/${{ matrix.target }}/release/${{ matrix.output }} ${{ matrix.output }} 86 | 87 | - name: Create archive (linux) 88 | if: ${{ matrix.os != 'macos-latest' && matrix.os != 'windows-latest' }} 89 | run: | 90 | tar -czvf dum-${{ matrix.target }}.${{ matrix.archive }} ${{ matrix.output }} 91 | sha256sum dum-${{ matrix.target }}.${{ matrix.archive }} > dum-${{ matrix.target }}-sha256sum.txt 92 | 93 | - name: Create archive (windows) 94 | if: ${{ matrix.os == 'windows-latest' }} 95 | run: | 96 | tar.exe -a -c -f dum-${{ matrix.target }}.${{ matrix.archive }} ${{ matrix.output }} 97 | sha256sum.exe dum-${{ matrix.target }}.${{ matrix.archive }} > dum-${{ matrix.target }}-sha256sum.txt 98 | 99 | - name: Create archive (macos) 100 | if: ${{ matrix.os == 'macos-latest' }} 101 | run: | 102 | zip dum-${{ matrix.target }}.${{ matrix.archive }} ${{ matrix.output }} 103 | sha256sum dum-${{ matrix.target }}.${{ matrix.archive }} > dum-${{ matrix.target }}-sha256sum.txt 104 | 105 | - name: Upload artifacts archive 106 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 107 | uses: actions/upload-artifact@v4 108 | with: 109 | name: dum-${{ matrix.target }}.${{ matrix.archive }} 110 | path: dum-${{ matrix.target }}.${{ matrix.archive }} 111 | 112 | - name: Upload artifacts checksum 113 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: dum-${{ matrix.target }}-sha256sum.txt 117 | path: dum-${{ matrix.target }}-sha256sum.txt 118 | 119 | - name: Upload binary to release 120 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 121 | uses: svenstaro/upload-release-action@v2 122 | with: 123 | repo_token: ${{ secrets.GITHUB_TOKEN }} 124 | file: dum-${{ matrix.target }}.${{ matrix.archive }} 125 | asset_name: dum-${{ matrix.target }}.${{ matrix.archive }} 126 | tag: ${{ github.ref }} 127 | overwrite: true 128 | 129 | - name: Upload checksum to release 130 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 131 | uses: svenstaro/upload-release-action@v2 132 | with: 133 | repo_token: ${{ secrets.GITHUB_TOKEN }} 134 | file: dum-${{ matrix.target }}-sha256sum.txt 135 | asset_name: dum-${{ matrix.target }}-sha256sum.txt 136 | tag: ${{ github.ref }} 137 | overwrite: true 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target 3 | node_modules 4 | npm-debug.log 5 | yarn-error.log 6 | .pnpm-debug.log 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.1.19 2 | 3 | - Revert v0.1.18, because some npm/pnpm flags are not supported by dum. 4 | 5 | ## v0.1.18 6 | 7 | - Replacing `npm run` `yarn` `pnpm run` `npx` `pnpx` in npm script with `dum`. 8 | - Tweak log format. 9 | 10 | ## v0.1.17 11 | 12 | - fixed a regression where `dum ` stopped working 13 | 14 | ## v0.1.16 15 | 16 | - Resolve `-c ` to absolute path, previously it always searches package.json from current directory even if `-c` contains `../`. 17 | - Allow flags in `install` `uninstall` `add` commands. 18 | 19 | ## v0.1.15 20 | 21 | - Forward args to `install` `uninstall` and `add` commands 22 | 23 | ## v0.1.14 24 | 25 | - Commands like `install` `uninstall` `add` are now handled before npm scripts, previously if there're no scripts or no package.json the command will not be executed. 26 | - Properly restore cursor after `ctrl-c`. 27 | 28 | ## v0.1.13 29 | 30 | - Ability to select npm scripts interactively, with `-i, --interactive` flag 31 | 32 | ## v0.1.12 33 | 34 | - Properly concat `$PATH` on Windows. [#24](https://github.com/egoist/dum/issues/24) 35 | 36 | ## v0.1.11 37 | 38 | - Resolve `node_modules/.bin` in parent directories too 39 | 40 | ## v0.1.10 41 | 42 | - Fallback to run binaries in `node_modules/.bin/` when specified script doesn't exist in `package.json`. 43 | - Available via Homebrew `brew install egoist/tap/dum` 44 | 45 | ## v0.1.9 46 | 47 | - Add `remove` command, mirrors `npm remove` `yarn remove` and `pnpm remove`. 48 | - Add `-c ` flag to change working directory. 49 | 50 | ## v0.1.8 51 | 52 | ### Fixes 53 | 54 | - Fetch `PATH` env at runtime. 55 | 56 | ## v0.1.7 57 | 58 | - Add command `add` 59 | 60 | ## v0.1.6 61 | 62 | - Forward args to `install` command. 63 | 64 | ## v0.1.5 65 | 66 | - Alias script `t` to `test`, so `dum t` and `dum test` are equivalent. 67 | - Add `install` command to automatically run `npm i`, `yarn` or `pnpm i` depending on the project. 68 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anstream" 25 | version = "0.6.18" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 28 | dependencies = [ 29 | "anstyle", 30 | "anstyle-parse", 31 | "anstyle-query", 32 | "anstyle-wincon", 33 | "colorchoice", 34 | "is_terminal_polyfill", 35 | "utf8parse", 36 | ] 37 | 38 | [[package]] 39 | name = "anstyle" 40 | version = "1.0.10" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 43 | 44 | [[package]] 45 | name = "anstyle-parse" 46 | version = "0.2.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 49 | dependencies = [ 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-query" 55 | version = "1.1.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 58 | dependencies = [ 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle-wincon" 64 | version = "3.0.7" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 67 | dependencies = [ 68 | "anstyle", 69 | "once_cell", 70 | "windows-sys", 71 | ] 72 | 73 | [[package]] 74 | name = "anyhow" 75 | version = "1.0.95" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 78 | 79 | [[package]] 80 | name = "bitflags" 81 | version = "1.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "2.8.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "cfg_aliases" 99 | version = "0.2.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 102 | 103 | [[package]] 104 | name = "colorchoice" 105 | version = "1.0.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 108 | 109 | [[package]] 110 | name = "console" 111 | version = "0.15.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" 114 | dependencies = [ 115 | "encode_unicode", 116 | "libc", 117 | "once_cell", 118 | "regex", 119 | "terminal_size", 120 | "unicode-width", 121 | "winapi", 122 | ] 123 | 124 | [[package]] 125 | name = "ctrlc" 126 | version = "3.4.5" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" 129 | dependencies = [ 130 | "nix", 131 | "windows-sys", 132 | ] 133 | 134 | [[package]] 135 | name = "dialoguer" 136 | version = "0.11.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 139 | dependencies = [ 140 | "console", 141 | "shell-words", 142 | "tempfile", 143 | "thiserror", 144 | "zeroize", 145 | ] 146 | 147 | [[package]] 148 | name = "dum" 149 | version = "0.1.20" 150 | dependencies = [ 151 | "ansi_term", 152 | "anyhow", 153 | "ctrlc", 154 | "dialoguer", 155 | "env_logger", 156 | "log", 157 | "path-absolutize", 158 | "serde_json", 159 | "shlex", 160 | ] 161 | 162 | [[package]] 163 | name = "encode_unicode" 164 | version = "0.3.6" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 167 | 168 | [[package]] 169 | name = "env_filter" 170 | version = "0.1.3" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 173 | dependencies = [ 174 | "log", 175 | "regex", 176 | ] 177 | 178 | [[package]] 179 | name = "env_logger" 180 | version = "0.11.6" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 183 | dependencies = [ 184 | "anstream", 185 | "anstyle", 186 | "env_filter", 187 | "humantime", 188 | "log", 189 | ] 190 | 191 | [[package]] 192 | name = "getrandom" 193 | version = "0.2.3" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 196 | dependencies = [ 197 | "cfg-if", 198 | "libc", 199 | "wasi", 200 | ] 201 | 202 | [[package]] 203 | name = "humantime" 204 | version = "2.1.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 207 | 208 | [[package]] 209 | name = "is_terminal_polyfill" 210 | version = "1.70.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 213 | 214 | [[package]] 215 | name = "itoa" 216 | version = "1.0.14" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 219 | 220 | [[package]] 221 | name = "libc" 222 | version = "0.2.169" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 225 | 226 | [[package]] 227 | name = "log" 228 | version = "0.4.25" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 231 | 232 | [[package]] 233 | name = "memchr" 234 | version = "2.4.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 237 | 238 | [[package]] 239 | name = "nix" 240 | version = "0.29.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 243 | dependencies = [ 244 | "bitflags 2.8.0", 245 | "cfg-if", 246 | "cfg_aliases", 247 | "libc", 248 | ] 249 | 250 | [[package]] 251 | name = "once_cell" 252 | version = "1.20.2" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 255 | 256 | [[package]] 257 | name = "path-absolutize" 258 | version = "3.1.1" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" 261 | dependencies = [ 262 | "path-dedot", 263 | ] 264 | 265 | [[package]] 266 | name = "path-dedot" 267 | version = "3.1.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" 270 | dependencies = [ 271 | "once_cell", 272 | ] 273 | 274 | [[package]] 275 | name = "ppv-lite86" 276 | version = "0.2.15" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" 279 | 280 | [[package]] 281 | name = "proc-macro2" 282 | version = "1.0.93" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 285 | dependencies = [ 286 | "unicode-ident", 287 | ] 288 | 289 | [[package]] 290 | name = "quote" 291 | version = "1.0.38" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 294 | dependencies = [ 295 | "proc-macro2", 296 | ] 297 | 298 | [[package]] 299 | name = "rand" 300 | version = "0.8.4" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 303 | dependencies = [ 304 | "libc", 305 | "rand_chacha", 306 | "rand_core", 307 | "rand_hc", 308 | ] 309 | 310 | [[package]] 311 | name = "rand_chacha" 312 | version = "0.3.1" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 315 | dependencies = [ 316 | "ppv-lite86", 317 | "rand_core", 318 | ] 319 | 320 | [[package]] 321 | name = "rand_core" 322 | version = "0.6.3" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 325 | dependencies = [ 326 | "getrandom", 327 | ] 328 | 329 | [[package]] 330 | name = "rand_hc" 331 | version = "0.3.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 334 | dependencies = [ 335 | "rand_core", 336 | ] 337 | 338 | [[package]] 339 | name = "redox_syscall" 340 | version = "0.2.10" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 343 | dependencies = [ 344 | "bitflags 1.3.2", 345 | ] 346 | 347 | [[package]] 348 | name = "regex" 349 | version = "1.5.4" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 352 | dependencies = [ 353 | "aho-corasick", 354 | "memchr", 355 | "regex-syntax", 356 | ] 357 | 358 | [[package]] 359 | name = "regex-syntax" 360 | version = "0.6.25" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 363 | 364 | [[package]] 365 | name = "remove_dir_all" 366 | version = "0.5.3" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 369 | dependencies = [ 370 | "winapi", 371 | ] 372 | 373 | [[package]] 374 | name = "ryu" 375 | version = "1.0.5" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 378 | 379 | [[package]] 380 | name = "serde" 381 | version = "1.0.217" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 384 | dependencies = [ 385 | "serde_derive", 386 | ] 387 | 388 | [[package]] 389 | name = "serde_derive" 390 | version = "1.0.217" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 393 | dependencies = [ 394 | "proc-macro2", 395 | "quote", 396 | "syn", 397 | ] 398 | 399 | [[package]] 400 | name = "serde_json" 401 | version = "1.0.136" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2" 404 | dependencies = [ 405 | "itoa", 406 | "memchr", 407 | "ryu", 408 | "serde", 409 | ] 410 | 411 | [[package]] 412 | name = "shell-words" 413 | version = "1.1.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 416 | 417 | [[package]] 418 | name = "shlex" 419 | version = "1.3.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 422 | 423 | [[package]] 424 | name = "syn" 425 | version = "2.0.96" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 428 | dependencies = [ 429 | "proc-macro2", 430 | "quote", 431 | "unicode-ident", 432 | ] 433 | 434 | [[package]] 435 | name = "tempfile" 436 | version = "3.2.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 439 | dependencies = [ 440 | "cfg-if", 441 | "libc", 442 | "rand", 443 | "redox_syscall", 444 | "remove_dir_all", 445 | "winapi", 446 | ] 447 | 448 | [[package]] 449 | name = "terminal_size" 450 | version = "0.1.17" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 453 | dependencies = [ 454 | "libc", 455 | "winapi", 456 | ] 457 | 458 | [[package]] 459 | name = "thiserror" 460 | version = "1.0.69" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 463 | dependencies = [ 464 | "thiserror-impl", 465 | ] 466 | 467 | [[package]] 468 | name = "thiserror-impl" 469 | version = "1.0.69" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 472 | dependencies = [ 473 | "proc-macro2", 474 | "quote", 475 | "syn", 476 | ] 477 | 478 | [[package]] 479 | name = "unicode-ident" 480 | version = "1.0.14" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 483 | 484 | [[package]] 485 | name = "unicode-width" 486 | version = "0.1.9" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 489 | 490 | [[package]] 491 | name = "utf8parse" 492 | version = "0.2.2" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 495 | 496 | [[package]] 497 | name = "wasi" 498 | version = "0.10.2+wasi-snapshot-preview1" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 501 | 502 | [[package]] 503 | name = "winapi" 504 | version = "0.3.9" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 507 | dependencies = [ 508 | "winapi-i686-pc-windows-gnu", 509 | "winapi-x86_64-pc-windows-gnu", 510 | ] 511 | 512 | [[package]] 513 | name = "winapi-i686-pc-windows-gnu" 514 | version = "0.4.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 517 | 518 | [[package]] 519 | name = "winapi-x86_64-pc-windows-gnu" 520 | version = "0.4.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 523 | 524 | [[package]] 525 | name = "windows-sys" 526 | version = "0.59.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 529 | dependencies = [ 530 | "windows-targets", 531 | ] 532 | 533 | [[package]] 534 | name = "windows-targets" 535 | version = "0.52.6" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 538 | dependencies = [ 539 | "windows_aarch64_gnullvm", 540 | "windows_aarch64_msvc", 541 | "windows_i686_gnu", 542 | "windows_i686_gnullvm", 543 | "windows_i686_msvc", 544 | "windows_x86_64_gnu", 545 | "windows_x86_64_gnullvm", 546 | "windows_x86_64_msvc", 547 | ] 548 | 549 | [[package]] 550 | name = "windows_aarch64_gnullvm" 551 | version = "0.52.6" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 554 | 555 | [[package]] 556 | name = "windows_aarch64_msvc" 557 | version = "0.52.6" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 560 | 561 | [[package]] 562 | name = "windows_i686_gnu" 563 | version = "0.52.6" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 566 | 567 | [[package]] 568 | name = "windows_i686_gnullvm" 569 | version = "0.52.6" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 572 | 573 | [[package]] 574 | name = "windows_i686_msvc" 575 | version = "0.52.6" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 578 | 579 | [[package]] 580 | name = "windows_x86_64_gnu" 581 | version = "0.52.6" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 584 | 585 | [[package]] 586 | name = "windows_x86_64_gnullvm" 587 | version = "0.52.6" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 590 | 591 | [[package]] 592 | name = "windows_x86_64_msvc" 593 | version = "0.52.6" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 596 | 597 | [[package]] 598 | name = "zeroize" 599 | version = "1.4.3" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" 602 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dum" 3 | version = "0.1.20" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "An npm scripts runner" 7 | authors = ["EGOIST <0x142857@gmail.com>"] 8 | homepage = "https://github.com/egoist/dum" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | serde_json = "1.0.136" 14 | dialoguer = "0.11.0" 15 | ansi_term = "0.12.1" 16 | ctrlc = "3.4.5" 17 | log = "0.4.25" 18 | env_logger = "0.11.6" 19 | path-absolutize = "3.1.1" 20 | anyhow = "1.0.95" 21 | shlex = "1.3.0" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 EGOIST <0x142857@gmail.com> (https://egoist.sh) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


2 | 3 |

4 | 5 |

6 | dum replaces npm run and npx.
Instead of waiting 200ms for your npm client to start, it will start immediately. 7 |
8 | 💛 You can help the author become a full-time open-source maintainer by sponsoring him on GitHub. 9 |

10 | 11 |
12 | 13 |

14 | CleanShot 2021-11-20 at 15 23 54@2x

15 | 16 |
17 | 18 | --- 19 | 20 |
21 | 22 | ## How 23 | 24 | This is written in Rust! (Or any compile-to-native language). 25 | 26 | Benchmark (`hyperfine "dum foo" "npm run foo" --warmup 10`): 27 | 28 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | 29 | | :------------ | ----------: | -------: | -------: | ----------: | 30 | | `dum foo` | 41.7 ± 1.2 | 39.8 | 44.6 | 1.00 | 31 | | `npm run foo` | 333.7 ± 2.0 | 330.0 | 336.0 | 8.01 ± 0.23 | 32 | 33 | ## Install 34 | 35 | ### Homebrew 36 | 37 | ```bash 38 | brew install egoist/tap/dum 39 | ``` 40 | 41 | ### Arch Linux AUR 42 | 43 | ```bash 44 | yay -S dum 45 | # or 46 | paru -S dum 47 | ``` 48 | 49 | Check for version info. 50 | 51 | ### Shell 52 | 53 | ```bash 54 | curl -sSL https://bina.egoist.dev/egoist/dum | sh 55 | ``` 56 | 57 | ### Cargo 58 | 59 | ```bash 60 | cargo install dum 61 | ``` 62 | 63 | ### Scoop 64 | 65 | ```shell 66 | scoop install dum 67 | ``` 68 | 69 | ### GitHub Releases 70 | 71 | [Download a release manually](https://github.com/egoist/dum/releases) and move it to `/usr/local/bin` manually. 72 | 73 | ## Usage 74 | 75 | `dum [...args_to_forward]`: Run npm scripts or scripts in `node_modules/.bin`, like `yarn run`, `npm run`, `npx`. 76 | 77 | If you want to pass flags to `dum` itself, like the `-c` flag to change directory, you should put it before the script name, like `dum -c another/directory script_name --forward some_flag`. 78 | 79 | Examples: 80 | 81 | ```bash 82 | dum some-npm-script 83 | 84 | dum some-npm-script --flags will --be forwarded 85 | # Like npx, but mush faster 86 | dum some-npm-package-cli-name --flags will --be forwarded 87 | 88 | # Change working directory 89 | dum -c packages/sub-package build 90 | 91 | # More 92 | dum --help 93 | ``` 94 | 95 | ### Install Packages 96 | 97 | Dum is not a package manager yet, but we forward `install`, `add`, `remove` commands to the package manager you're currently using: 98 | 99 | ```bash 100 | # Run `npm i` or `yarn` or `pnpm i` depending on the project 101 | dum install # or `dum i` 102 | # Like above but add packages 103 | dum add react vue -D 104 | 105 | dum remove react vue 106 | ``` 107 | 108 | We detect the package manager automatically by checking for lock files in the current directory. If no lock file is found, we ask you to select a package manager first. 109 | 110 | ## Limitations 111 | 112 | - [package.json vars](https://docs.npmjs.com/cli/v8/using-npm/scripts#packagejson-vars) are not supported, I personally never used it, if you believe it's necessary, please [leave a comment here](https://github.com/egoist/dum/issues/2). 113 | 114 | ## Inspiration 115 | 116 | I want to try and learn Rust so I made this. Inspired by [bun](https://bun.sh/). 117 | 118 | ## Development 119 | 120 | ```bash 121 | cargo run -- <...args to test> 122 | ``` 123 | 124 | ## Sponsors 125 | 126 | [![sponsors](https://sponsors-images.egoist.dev/sponsors.svg)](https://github.com/sponsors/egoist) 127 | 128 | ## License 129 | 130 | MIT © [EGOIST](https://github.com/sponsors/egoist) 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "foo": "node stuff/example-script", 4 | "test": "echo \"hello test\" && npm run foo -- arg", 5 | "build-mac": "cargo build --release --target x86_64-apple-darwin && cargo build --release --target aarch64-apple-darwin", 6 | "build-win": "cargo build --release --target x86_64-pc-windows-msvc", 7 | "build-lin": "cargo build --release --target x86_64-unknown-linux-gnu && cargo build --release --target armv7-unknown-linux-gnueabihf" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | extern crate path_absolutize; 2 | 3 | use std::env; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::exit; 6 | 7 | use path_absolutize::Absolutize; 8 | 9 | #[derive(Debug)] 10 | pub struct AppArgs { 11 | pub script_name: String, 12 | pub forwarded: Vec, 13 | pub change_dir: PathBuf, 14 | pub command: String, 15 | pub interactive: bool, 16 | pub silent: bool, 17 | } 18 | 19 | pub const COMMANDS_TO_FORWARD: &[&str] = &["install", "i", "add", "remove", "uninstall"]; 20 | 21 | pub fn parse_args(args_vec: Vec) -> AppArgs { 22 | let mut args_iter = args_vec.into_iter(); 23 | 24 | let mut args = AppArgs { 25 | script_name: "".to_string(), 26 | change_dir: PathBuf::from(env::current_dir().as_ref().unwrap()), 27 | forwarded: vec![], 28 | command: "".to_string(), 29 | interactive: false, 30 | silent: false, 31 | }; 32 | 33 | loop { 34 | let arg = args_iter.next(); 35 | match arg { 36 | Some(v) => { 37 | if v == "--" { 38 | args.forwarded.extend(args_iter); 39 | break; 40 | } 41 | if v.starts_with('-') { 42 | if args.script_name.is_empty() 43 | && (args.command.is_empty() || args.command == "run") 44 | { 45 | match v.as_ref() { 46 | "-c" => { 47 | let dir = match args_iter.next() { 48 | Some(v) => PathBuf::from(Path::new(&v).absolutize().unwrap()), 49 | None => { 50 | println!("No directory specified"); 51 | exit(1); 52 | } 53 | }; 54 | if !dir.exists() { 55 | println!("Error: directory {} does not exist", dir.display()); 56 | std::process::exit(1); 57 | } 58 | args.change_dir = dir; 59 | } 60 | "-i" | "--interactive" => { 61 | args.interactive = true; 62 | } 63 | "-s" | "--silent" => { 64 | args.silent = true; 65 | } 66 | "-h" | "--help" => { 67 | print!("{}", get_help()); 68 | std::process::exit(0); 69 | } 70 | "-v" | "--version" => { 71 | println!("{}", get_version()); 72 | std::process::exit(0); 73 | } 74 | _ => { 75 | println!("Unknown flag: {}", v); 76 | exit(1); 77 | } 78 | } 79 | } else { 80 | args.forwarded.push(v); 81 | } 82 | } else if args.command.is_empty() 83 | && (COMMANDS_TO_FORWARD.contains(&v.as_str()) || v == "run") 84 | { 85 | args.command = match v.as_ref() { 86 | "i" => "install".to_string(), 87 | _ => v.to_string(), 88 | }; 89 | } else if (args.command.is_empty() || args.command == "run") 90 | && args.script_name.is_empty() 91 | { 92 | args.command = "run".to_string(); 93 | args.script_name = match v.as_ref() { 94 | "t" => "test".to_string(), 95 | _ => v.to_string(), 96 | }; 97 | } else { 98 | if args.interactive { 99 | eprintln!("You can't pass arguments to interactive mode"); 100 | exit(1); 101 | } 102 | args.forwarded.push(v); 103 | } 104 | } 105 | None => break, 106 | } 107 | } 108 | args 109 | } 110 | 111 | fn get_version() -> String { 112 | env!("CARGO_PKG_VERSION").to_string() 113 | } 114 | 115 | pub fn get_help() -> String { 116 | format!( 117 | "\ 118 | dum v{} 119 | 120 | USAGE: 121 | dum [OUR_FLAGS] [SCRIPT_NAME] [SCRIPT_ARGS] 122 | 123 | COMMANDS: 124 | Run an npm script (like npm run) or a script in node_modules/.bin (like npx) 125 | run Show a list of available scripts 126 | run Run an npm script 127 | add Add packages to the current project 128 | i, install Install dependencies 129 | remove Remove packages from the current project 130 | t, test Run test script in nearest package.json 131 | [script] Run scripts in nearest package.json 132 | 133 | FLAGS: 134 | -c Change working directory 135 | -i, --interactive Interactive mode 136 | -s, --silent Suppress the script info output 137 | -h, --help Prints help information 138 | -v, --version Prints version number 139 | ", 140 | get_version() 141 | ) 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | use path_absolutize::Absolutize; 148 | 149 | macro_rules! vec_of_strings { 150 | // match a list of expressions separated by comma: 151 | ($($str:expr),*) => ({ 152 | // create a Vec with this list of expressions, 153 | // calling String::from on each: 154 | vec![$(String::from($str),)*] as Vec 155 | }); 156 | } 157 | 158 | #[test] 159 | fn test_parse() { 160 | let args = parse_args(vec_of_strings!["a", "b", "-c", "-d", "foo", "bar"]); 161 | assert_eq!(args.script_name, "a".to_string()); 162 | assert_eq!(args.forwarded, vec!["b", "-c", "-d", "foo", "bar"]); 163 | } 164 | 165 | #[test] 166 | fn test_parse_own_flags() { 167 | let args = parse_args(vec_of_strings!["-c", ".", "a"]); 168 | assert_eq!(args.script_name, "a".to_string()); 169 | assert_eq!(args.change_dir, Path::new(".").absolutize().unwrap()); 170 | assert_eq!(args.forwarded, Vec::<&str>::new()); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/install.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::str::FromStr; 3 | 4 | use anyhow::{anyhow, Error}; 5 | 6 | use crate::prompt; 7 | 8 | pub enum PackageManager { 9 | Yarn, 10 | Npm, 11 | Pnpm, 12 | Bun, 13 | } 14 | 15 | impl ToString for PackageManager { 16 | fn to_string(&self) -> String { 17 | match self { 18 | PackageManager::Yarn => "yarn".to_string(), 19 | PackageManager::Npm => "npm".to_string(), 20 | PackageManager::Pnpm => "pnpm".to_string(), 21 | PackageManager::Bun => "bun".to_string(), 22 | } 23 | } 24 | } 25 | 26 | impl FromStr for PackageManager { 27 | type Err = Error; 28 | 29 | fn from_str(name: &str) -> Result { 30 | match name { 31 | "yarn" => Ok(PackageManager::Yarn), 32 | "npm" => Ok(PackageManager::Npm), 33 | "pnpm" => Ok(PackageManager::Pnpm), 34 | "bun" => Ok(PackageManager::Bun), 35 | _ => Err(anyhow!("Parse package manager error")), 36 | } 37 | } 38 | } 39 | 40 | // A function to guess package manager by looking for lock file in current directory only 41 | // If yarn.lock is found, it's likely to be a yarn project 42 | // If package-lock.json is found, it's likely to be a npm project 43 | // If pnpm-lock.yaml is found, it's likely to be a pnpm project 44 | // If none of the above is found, return None 45 | pub fn guess_package_manager(dir: &Path) -> Option { 46 | let lock_file = dir.join("yarn.lock"); 47 | if lock_file.exists() { 48 | return Some(PackageManager::Yarn); 49 | } 50 | 51 | let lock_file = dir.join("package-lock.json"); 52 | if lock_file.exists() { 53 | return Some(PackageManager::Npm); 54 | } 55 | 56 | let lock_file = dir.join("pnpm-lock.yaml"); 57 | if lock_file.exists() { 58 | return Some(PackageManager::Pnpm); 59 | } 60 | 61 | // bun supports both bun.lockb and bun.lock 62 | let lock_file = dir.join("bun.lockb"); 63 | if lock_file.exists() { 64 | return Some(PackageManager::Bun); 65 | } 66 | 67 | let lock_file = dir.join("bun.lock"); 68 | if lock_file.exists() { 69 | return Some(PackageManager::Bun); 70 | } 71 | 72 | let items = vec!["pnpm", "npm", "yarn", "bun"]; 73 | match prompt::select("Which package manager do you want to use?", items) { 74 | Some(pm) => PackageManager::from_str(&pm).ok(), 75 | None => None, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod install; 3 | mod prompt; 4 | mod run; 5 | 6 | use std::env; 7 | 8 | fn main() { 9 | env_logger::init(); 10 | prompt::handle_ctrlc(); 11 | 12 | let args_vec: Vec = env::args().skip(1).collect(); 13 | let args = args::parse_args(args_vec); 14 | run::run(args); 15 | } 16 | -------------------------------------------------------------------------------- /src/prompt.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::console::Term; 2 | use dialoguer::{theme::ColorfulTheme, Input, Select}; 3 | use std::process::exit; 4 | 5 | fn show_cursor() { 6 | Term::stderr().show_cursor().expect("failed to show cursor"); 7 | } 8 | 9 | pub fn handle_ctrlc() { 10 | ctrlc::set_handler(move || { 11 | show_cursor(); 12 | exit(1); 13 | }) 14 | .expect("Error setting Ctrl-C handler"); 15 | } 16 | 17 | pub fn select(message: &str, script_names: Vec<&str>) -> Option { 18 | let selection = Select::with_theme(&ColorfulTheme::default()) 19 | .with_prompt(message) 20 | .items(&script_names) 21 | .default(0) 22 | .interact_on_opt(&Term::stderr()) 23 | .ok()?; 24 | 25 | show_cursor(); 26 | 27 | selection?; 28 | 29 | Some(script_names[selection.unwrap()].to_string()) 30 | } 31 | 32 | pub fn input(message: &str) -> Option { 33 | let input = Input::::new() 34 | .with_prompt(message) 35 | .allow_empty(true) 36 | .with_initial_text("") 37 | .interact_text_on(&Term::stderr()) 38 | .ok(); 39 | 40 | show_cursor(); 41 | 42 | input 43 | } 44 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use crate::{args, install, prompt}; 2 | use shlex; 3 | 4 | use ansi_term::{ 5 | Color::{Purple, Red}, 6 | Style, 7 | }; 8 | use log::debug; 9 | use serde_json::Value; 10 | use std::collections::HashMap; 11 | use std::env; 12 | use std::fs::read_to_string; 13 | use std::path::{Path, PathBuf}; 14 | use std::process::{exit, Command}; 15 | 16 | // Get PATH env and join it with bin_dir 17 | fn get_path_env(bin_dirs: Vec) -> String { 18 | let path = PathBuf::from(env::var("PATH").unwrap_or_default()); 19 | env::join_paths( 20 | bin_dirs 21 | .iter() 22 | .chain(env::split_paths(&path).collect::>().iter()), 23 | ) 24 | .ok() 25 | .unwrap() 26 | .into_string() 27 | .unwrap() 28 | } 29 | 30 | // A function to find the closest file 31 | // Starting from current directory 32 | // Recursively until it finds the file or reach root directory 33 | fn find_closest_files(_current_dir: &Path, name: &str, stop_on_first: bool) -> Vec { 34 | let mut closest_file: Vec = Vec::new(); 35 | let mut current_dir = Path::new(_current_dir); 36 | loop { 37 | let path = current_dir.join(name); 38 | 39 | if path.exists() { 40 | closest_file.push(path); 41 | if stop_on_first { 42 | break; 43 | } 44 | } 45 | match current_dir.parent() { 46 | Some(p) => current_dir = p, 47 | None => break, 48 | } 49 | } 50 | 51 | closest_file 52 | } 53 | 54 | struct RunOptions { 55 | envs: HashMap, 56 | current_dir: PathBuf, 57 | } 58 | 59 | fn run_command(script: &str, args: &[&str], options: &RunOptions) { 60 | let mut command = { 61 | if cfg!(target_os = "windows") { 62 | let mut command = Command::new("cmd"); 63 | command.arg("/C").arg(script); 64 | command 65 | } else { 66 | let mut command = Command::new("sh"); 67 | command 68 | .arg("-c") 69 | .arg(format!("{} \"$@\"", script)) 70 | .arg("sh"); 71 | command 72 | } 73 | }; 74 | 75 | // assign the value of options.current_dir to current_dir 76 | let status = command 77 | .args(args) 78 | .envs(&options.envs) 79 | .current_dir(&options.current_dir) 80 | .status() 81 | .expect("failed to execute the command"); 82 | 83 | exit(status.code().unwrap_or(1)); 84 | } 85 | 86 | fn resolve_bin_path(bin_name: &str, dirs: &[PathBuf]) -> Option { 87 | for dir in dirs { 88 | let path = dir.join(bin_name); 89 | if path.exists() { 90 | return Some(path); 91 | } 92 | } 93 | 94 | None 95 | } 96 | 97 | // Print the script name / script 98 | fn print_script_info(script_name: &str, script: &str, forwarded: &[&str]) { 99 | println!( 100 | "{} {}", 101 | Purple.dimmed().paint("$"), 102 | Style::new().bold().dimmed().paint(script_name) 103 | ); 104 | println!( 105 | "{} {} {}", 106 | Purple.dimmed().paint("$"), 107 | Style::new().bold().dimmed().paint(script), 108 | Style::new() 109 | .bold() 110 | .dimmed() 111 | .paint(shlex::try_join(forwarded.to_vec()).unwrap()), 112 | ); 113 | } 114 | 115 | pub fn run(app_args: args::AppArgs) { 116 | if args::COMMANDS_TO_FORWARD.contains(&app_args.command.as_str()) { 117 | debug!("Running command {}", app_args.command); 118 | let pm = install::guess_package_manager(&app_args.change_dir); 119 | 120 | if pm.is_none() { 121 | eprintln!("Aborted."); 122 | exit(1); 123 | } 124 | let args = vec![app_args.command.as_str()]; 125 | let args: Vec<&str> = args 126 | .into_iter() 127 | .chain(app_args.forwarded.iter().map(|s| s.as_str())) 128 | .collect(); 129 | 130 | run_command( 131 | pm.unwrap().to_string().as_str(), 132 | &args, 133 | &RunOptions { 134 | current_dir: app_args.change_dir.clone(), 135 | envs: HashMap::new(), 136 | }, 137 | ); 138 | return; 139 | } 140 | 141 | debug!("change dir to {}", app_args.change_dir.display()); 142 | let pkg_paths = find_closest_files(&app_args.change_dir, "package.json", true); 143 | let pkg_path = if pkg_paths.is_empty() { 144 | eprintln!("No package.json found"); 145 | exit(1); 146 | } else { 147 | pkg_paths[0].clone() 148 | }; 149 | 150 | debug!("Found package.json at {}", pkg_path.display()); 151 | // The current_dir to execute npm scripts 152 | let execute_dir = PathBuf::from(pkg_path.parent().unwrap()); 153 | debug!("execute_dir: {:?}", execute_dir); 154 | 155 | let node_modules_dirs = find_closest_files(&app_args.change_dir, "node_modules", false); 156 | let bin_dirs = node_modules_dirs 157 | .iter() 158 | .map(|dir| dir.join(".bin")) 159 | .collect::>(); 160 | 161 | let contents = read_to_string(pkg_path).expect("failed to read package.json"); 162 | let v: Value = serde_json::from_str(&contents).expect("failed to parse package.json"); 163 | 164 | let scripts = v["scripts"].as_object(); 165 | let mut script_name = app_args.script_name; 166 | let mut forwarded = app_args.forwarded; 167 | 168 | if !app_args.interactive && app_args.command == "run" && script_name.is_empty() { 169 | match scripts { 170 | Some(scripts) => { 171 | println!("\n{}:\n", Style::new().bold().paint("Available scripts")); 172 | for (name, value) in scripts { 173 | println!("{}", Purple.paint(name)); 174 | println!(" {}", value.as_str().unwrap()); 175 | } 176 | return; 177 | } 178 | None => { 179 | eprintln!("No scripts found"); 180 | exit(1); 181 | } 182 | } 183 | } 184 | 185 | if !script_name.is_empty() && app_args.interactive { 186 | eprintln!("You can't specify script name in interactive mode"); 187 | exit(1); 188 | } 189 | 190 | if script_name.is_empty() { 191 | if !app_args.interactive { 192 | println!("No script name specified.\n"); 193 | println!("{}", args::get_help()); 194 | return; 195 | } 196 | 197 | if scripts.is_none() { 198 | eprintln!("No scripts found in package.json"); 199 | exit(1); 200 | } 201 | 202 | // Choose an script interactively 203 | // Convert keys of scripts to a vector of &str 204 | let names_vec = scripts 205 | .unwrap() 206 | .keys() 207 | .map(|k| k.as_str()) 208 | .collect::>(); 209 | script_name = match prompt::select("Select an npm script to run", names_vec) { 210 | Some(name) => name, 211 | None => { 212 | println!("No script selected."); 213 | return; 214 | } 215 | }; 216 | let mut arguments: Option> = 217 | match prompt::input("Enter arguments to pass to the script") { 218 | Some(args) => shlex::split(&args), 219 | None => { 220 | println!("Aborted."); 221 | return; 222 | } 223 | }; 224 | while let None = arguments { 225 | eprintln!("Error while parsing arguments: please check the the validity of the arguments and try again"); 226 | arguments = match prompt::input("Enter arguments to pass to the script") { 227 | Some(args) => shlex::split(&args), 228 | None => { 229 | println!("Aborted."); 230 | return; 231 | } 232 | }; 233 | } 234 | forwarded.extend(arguments.unwrap()); 235 | } 236 | 237 | let forwarded: Vec<&str> = forwarded.iter().map(|s| s.as_str()).collect(); 238 | let npm_script = scripts 239 | .and_then(|s| s.get(script_name.as_str())) 240 | .map(|script| { 241 | let script = script.as_str().map(|script| script.to_string()); 242 | script.unwrap_or_default() 243 | }); 244 | if let Some(script) = npm_script { 245 | if !app_args.silent { 246 | print_script_info(&script_name, &script, &forwarded); 247 | } 248 | let envs = HashMap::from([("PATH".to_string(), get_path_env(bin_dirs))]); 249 | run_command( 250 | script.as_str(), 251 | &forwarded, 252 | &RunOptions { 253 | current_dir: execute_dir, 254 | envs, 255 | }, 256 | ); 257 | return; 258 | } 259 | let resolved_bin = resolve_bin_path(script_name.as_str(), &bin_dirs); 260 | if let Some(bin_path) = resolved_bin { 261 | if !app_args.silent { 262 | print_script_info(&script_name, bin_path.to_str().unwrap(), &forwarded); 263 | } 264 | let envs = HashMap::from([("PATH".to_string(), get_path_env(bin_dirs))]); 265 | run_command( 266 | bin_path.to_str().unwrap(), 267 | &forwarded, 268 | &RunOptions { 269 | current_dir: execute_dir, 270 | envs, 271 | }, 272 | ); 273 | return; 274 | } 275 | 276 | // TODO: a custom logger module 277 | println!("{}", Red.normal().paint("No script found.")); 278 | println!("To see a list of scripts, run `dum run`"); 279 | exit(1); 280 | } 281 | -------------------------------------------------------------------------------- /stuff/example-script.js: -------------------------------------------------------------------------------- 1 | console.log("from example", process.argv.slice(2)) 2 | --------------------------------------------------------------------------------