Repository: axodotdev/cargo-dist
Pull request #216 (2023-04-16 20:50:18)

Update docs to mention M1 macOS as a supported target


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1a2af33..be3e6f73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# Version 0.0.6 (under development)
+
+TODO: generate proper release notes for:
+
+* fix typo at README.md: https://github.com/axodotdev/cargo-dist/pull/203
+* factor out and use axoproject: https://github.com/axodotdev/cargo-dist/pull/207
+* factor out and use axocli: https://github.com/axodotdev/cargo-dist/pull/209
+* unlocked archive formats: https://github.com/axodotdev/cargo-dist/pull/211
+* npm installer: https://github.com/axodotdev/cargo-dist/pull/210
+
+
 # Version 0.0.5 (2023-03-15)
 
 This is a bug-fix release for an issue with cross-platform line endings that affected
diff --git a/Cargo.lock b/Cargo.lock
index ff856094..c82f3dd6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -51,42 +51,51 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.2.6"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f"
+checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371"
 dependencies = [
  "anstyle",
  "anstyle-parse",
+ "anstyle-query",
  "anstyle-wincon",
- "concolor-override",
- "concolor-query",
+ "colorchoice",
  "is-terminal",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "0.3.5"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.1.1"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116"
+checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
 dependencies = [
  "utf8parse",
 ]
 
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "anstyle-wincon"
-version = "0.2.0"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa"
+checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd"
 dependencies = [
  "anstyle",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -159,7 +168,7 @@ dependencies = [
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
  "object",
  "rustc-demangle",
 ]
@@ -256,7 +265,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist"
-version = "0.0.5"
+version = "0.0.6-prerelease.2"
 dependencies = [
  "axocli",
  "axoproject",
@@ -285,7 +294,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist-schema"
-version = "0.0.5"
+version = "0.0.6-prerelease.2"
 dependencies = [
  "camino",
  "insta",
@@ -306,9 +315,9 @@ dependencies = [
 
 [[package]]
 name = "cargo_metadata"
-version = "0.15.3"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08a1ec454bc3eead8719cb56e15dbbfecdbc14e4b3a3ae4936cc6e31f5fc0d07"
+checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
 dependencies = [
  "camino",
  "cargo-platform",
@@ -329,9 +338,9 @@ dependencies = [
 
 [[package]]
 name = "cfg-expr"
-version = "0.13.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a327683d7499ecc47369531a679fe38acdd300e09bf8c852d08b1e10558622bd"
+checksum = "732ffa2b10f89dd59538349855f3eaf4032e909361ac4917a52fb43cc70fccf0"
 dependencies = [
  "smallvec",
  "target-lexicon",
@@ -354,9 +363,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.2.1"
+version = "4.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3"
+checksum = "9b802d85aaf3a1cdb02b224ba472ebdea62014fccfcb269b95a4d76443b5ee5a"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -375,9 +384,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.2.1"
+version = "4.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f"
+checksum = "14a1a858f532119338887a4b8e1af9c60de8249cd7bafd68036a489e261e37b6"
 dependencies = [
  "anstream",
  "anstyle",
@@ -395,7 +404,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -410,6 +419,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
 [[package]]
 name = "combine"
 version = "4.6.6"
@@ -420,21 +435,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "concolor-override"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f"
-
-[[package]]
-name = "concolor-query"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf"
-dependencies = [
- "windows-sys 0.45.0",
-]
-
 [[package]]
 name = "console"
 version = "0.15.5"
@@ -474,9 +474,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
 dependencies = [
  "cfg-if",
  "crossbeam-utils",
@@ -574,13 +574,13 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
 [[package]]
 name = "errno"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -603,22 +603,31 @@ dependencies = [
  "flume",
  "half",
  "lebe",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
  "rayon-core",
  "smallvec",
  "zune-inflate",
 ]
 
+[[package]]
+name = "fdeflate"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
+dependencies = [
+ "simd-adler32",
+]
+
 [[package]]
 name = "filetime"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
+checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
 dependencies = [
  "cfg-if",
  "libc",
  "redox_syscall",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -634,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
 dependencies = [
  "crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
 ]
 
 [[package]]
@@ -683,9 +692,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.8"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -845,25 +854,25 @@ dependencies = [
 
 [[package]]
 name = "io-lifetimes"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
 dependencies = [
  "hermit-abi 0.3.1",
  "libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "is-terminal"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
 dependencies = [
  "hermit-abi 0.3.1",
  "io-lifetimes",
  "rustix",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -928,9 +937,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
 
 [[package]]
 name = "libc"
-version = "0.2.140"
+version = "0.2.141"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
+checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
 
 [[package]]
 name = "linked-hash-map"
@@ -1018,7 +1027,7 @@ checksum = "8842972f23939443013dfd3720f46772b743e86f1a81d120d4b6fb090f87de1c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -1036,6 +1045,16 @@ dependencies = [
  "adler",
 ]
 
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+ "simd-adler32",
+]
+
 [[package]]
 name = "nanorand"
 version = "0.7.0"
@@ -1237,14 +1256,15 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
 
 [[package]]
 name = "png"
-version = "0.17.7"
+version = "0.17.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa"
 dependencies = [
  "bitflags",
  "crc32fast",
+ "fdeflate",
  "flate2",
- "miniz_oxide",
+ "miniz_oxide 0.7.1",
 ]
 
 [[package]]
@@ -1330,22 +1350,22 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
 name = "rustc-demangle"
-version = "0.1.22"
+version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
 
 [[package]]
 name = "rustix"
-version = "0.37.7"
+version = "0.37.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d"
+checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
 dependencies = [
  "bitflags",
  "errno",
  "io-lifetimes",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1395,22 +1415,22 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.159"
+version = "1.0.160"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.159"
+version = "1.0.160"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
+checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -1426,9 +1446,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.95"
+version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
 dependencies = [
  "itoa",
  "ryu",
@@ -1558,9 +1578,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.13"
+version = "2.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
+checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1586,9 +1606,9 @@ checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5"
 
 [[package]]
 name = "target-spec"
-version = "1.3.1"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb0303f2cecb4171c439135b28c33fe9da7b9fd7816fe674761834da70c9778"
+checksum = "0bf4306559bd50cb358e7af5692694d6f6fad95cf2c0bea2571dd419f5298e12"
 dependencies = [
  "cfg-expr",
  "guppy-workspace-hack",
@@ -1633,7 +1653,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -1937,37 +1957,37 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "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-sys"
-version = "0.45.0"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
  "windows-targets",
 ]
 
 [[package]]
 name = "windows-targets"
-version = "0.42.2"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
 ]
 
 [[package]]
@@ -1976,42 +1996,84 @@ version = "0.42.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
 [[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
 [[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
 [[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
 [[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
 [[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
 [[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
 [[package]]
 name = "xattr"
 version = "0.2.3"
@@ -2080,9 +2142,9 @@ dependencies = [
 
 [[package]]
 name = "zstd-sys"
-version = "2.0.7+zstd.1.5.4"
+version = "2.0.8+zstd.1.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5"
+checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
 dependencies = [
  "cc",
  "libc",
diff --git a/book/src/artifacts.md b/book/src/artifacts.md
index 373e5a84..f7524b61 100644
--- a/book/src/artifacts.md
+++ b/book/src/artifacts.md
@@ -20,8 +20,7 @@ Some notes on how we build your executables:
 * We currently [always build with default features][features-issue]
 * When targeting windows-msvc we will unconditionally [append "-Ctarget-feature=+crt-static"][crt-static-rfc] to your RUSTFLAGS, which should just be the default for rustc but isn't for legacy reasons
 * We don't yet [support cross-compilation][cross-issue]. We'll faithfully attempt the compile by passing `--target` to cargo as instructed but it will probably just fail.
-    * [arm64-apple-darwin (apple silicon) is slated for 0.0.4][arm64-apple-issue]
-    * [linux-musl is slated for 0.0.4][musl-issue]
+    * [linux-musl is slated for a future version][musl-issue]
 
 ## symbols
 
diff --git a/book/src/concepts.md b/book/src/concepts.md
index b548eae0..6ce0d429 100644
--- a/book/src/concepts.md
+++ b/book/src/concepts.md
@@ -52,7 +52,7 @@ ci = ["github"]
 # The installers to generate for each app
 installers = ["shell", "powershell"]
 # Target platforms to build apps for (Rust target-triple syntax)
-targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc"]
+targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 
 # The profile that 'cargo dist' will build with
 [profile.dist]
@@ -247,4 +247,4 @@ CI will just invoke cargo-dist in the following sequence:
 [defining-your-apps-section]: #defining-your-apps
 [cargo-metadata]: https://doc.rust-lang.org/cargo/commands/cargo-metadata.html
 [workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html
-[guide]: ./guide.html
\ No newline at end of file
+[guide]: ./guide.html
diff --git a/book/src/config.md b/book/src/config.md
index bdd93dab..acceadf8 100644
--- a/book/src/config.md
+++ b/book/src/config.md
@@ -91,13 +91,14 @@ Example: `targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-
 
 This is a list of [target platforms][platforms] you want your application(s) to be built for. In principle this can be overridden on a per-package basis but that is not well tested.
 
-In v0.0.3 the only properly supported choices are:
+In v0.0.5 the only properly supported choices are:
 
 * x64 macOS: "x86_64-apple-darwin"
 * x64 Windows: "x86_64-pc-windows-msvc"
 * x64 Linux: "x86_64-unknown-linux-gnu"
+* arm64 macOS (Apple silicon): "aarch64-apple-darwin" (supported added in v0.0.4)
 
-v0.0.4 should hopefully introduce proper support for important targets like "arm64 macos (apple silicon)" and "musl linux".
+Future versions should hopefully introduce proper support for important targets like "musl linux".
 
 By default all runs of `cargo-dist` will be trying to handle all platforms specified here at once. If you specify `--target=...` on the CLI this will focus the run to only those platforms. As discussed in [concepts][], this cannot be used to specify platforms that are not listed in `metadata.dist`, to ensure different runs agree on the maximum set of platforms.
 
@@ -128,6 +129,29 @@ Example: `auto-includes = false`
 
 Allows you to specify whether cargo-dist should auto-include README, (UN)LICENSE, and CHANGELOG/RELEASES files in [executable-zips][]. Defaults to true.
 
+### windows-archive
+
+Example: `windows-archive = ".tar.gz"`
+
+Allows you to specify the file format to use for [executable-zips][] that target windows. The default is
+".zip". Supported values:
+
+* ".zip"
+* ".tar.gz"
+* ".tar.xz"
+* ".tar.zstd"
+
+See also unix-archive below.
+
+### unix-archive
+
+Example: `unix-archive = ".tar.gz"`
+
+Allows you to specify the file format to use for [executable-zips][] that target not-windows. The default is
+".tar.xz". See "windows-archive" above for a complete list of supported values.
+
+
+
 ### dist
 
 Example: `dist = false`
@@ -158,4 +182,4 @@ Caveat: the default "host" Artifact Mode does something fuzzier with `--target`
 [semver-version]: https://docs.rs/semver/latest/semver/struct.Version.html
 [platforms]: https://doc.rust-lang.org/nightly/rustc/platform-support.html
 [executable-zips]: ./artifacts.md#executable-zip
-[artifacts-modes]: ./concepts.md#artifact-modes-selecting-artifacts
\ No newline at end of file
+[artifacts-modes]: ./concepts.md#artifact-modes-selecting-artifacts
diff --git a/book/src/simple-guide.md b/book/src/simple-guide.md
index 3b27c6f0..1d0712c4 100644
--- a/book/src/simple-guide.md
+++ b/book/src/simple-guide.md
@@ -35,7 +35,7 @@ rust-toolchain-version = "1.67.1"
 # CI backends to support (see 'cargo dist generate-ci')
 ci = ["github"]
 # Target platforms to build apps for (Rust target-triple syntax)
-targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc"]
+targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ```
 
 ### The "dist" Profile
@@ -58,7 +58,7 @@ Next let's talk about `[workspace.metadata.dist]`. Cargo allows other tools to i
 
 `ci = ["github"]` is just `init` faithfully recording the fact that we passed it `--ci=github`. This lets subsequent runs of [generate-ci][] know what CI scripts to generate. Its presence also enables certain Github-specific features like generating the body for a Github Release and telling installers to fetch binaries from a Github Release. ("github" is currently the only supported CI backend.)
 
-`targets = ...` is listing the [platforms][] to build your application for. In this case, because we didn't specify the targets with `--target`, [init][] has selected the "recommended desktop suite": "x64 linux", "x64 macos", and "x64 windows". In v0.0.3 these are the only properly supported choices, because we wanted to get the core of cargo-dist solid first. v0.0.4 should hopefully introduce proper support for important targets like "arm64 macos (apple silicon)" and "musl linux".
+`targets = ...` is listing the [platforms][] to build your application for. In this case, because we didn't specify the targets with `--target`, [init][] has selected the "recommended desktop suite": "x64 linux", "x64 macos", "x64 windows", and "arm64 macos (Apple silicon)". In v0.0.5 these are the only properly supported choices, because we wanted to get the core of cargo-dist solid first. Future versions should hopefully introduce proper support for important targets like "musl linux".
 
 The most important thing that this config *doesn't* have that you might want to include is "[installers][]". You can either manually add them to the [config][] or have [init][] do it for you with the `--installer` flag. Installers are intentionally excluded here to keep this example focused.
 
@@ -101,9 +101,10 @@ This output has two parts: "analyzing workspace" and "announcing"
 
 "analyzing workspace" describes what cargo-dist found in your workspace. In this case there's a package called "my-app" with a [binary target][bin] of the same name.
 
-"announcing v0.1.0" tells us the tag that should be pushed to announce a release of the current workspace ("v0.1.0"). Underneath it we see that the announcement will include "my-app 0.1.0" as expected. Underneath that we see 3 artifacts will be produced and uploaded:
+"announcing v0.1.0" tells us the tag that should be pushed to announce a release of the current workspace ("v0.1.0"). Underneath it we see that the announcement will include "my-app 0.1.0" as expected. Underneath that we see 4 artifacts will be produced and uploaded:
 
-* The macOS build: my-app-v0.1.0-x86_64-apple-darwin.tar.xz
+* The Intel macOS build: my-app-v0.1.0-x86_64-apple-darwin.tar.xz
+* The M1 macOS build: my-app-v0.1.0-aarch64-apple-darwin.tar.xz
 * The Windows build: my-app-v0.1.0-x86_64-pc-windows-msvc.zip
 * The Linux build: my-app-v0.1.0-x86_64-unknown-linux-gnu.tar.xz
 
@@ -201,4 +202,4 @@ parse-changelog also has support for a special `# Unreleased` heading, but we do
 [build]: ./cli.md#cargo-dist-build
 [artifact-modes]: ./concepts.md#artifact-modes-selecting-artifacts
 [parse-changelog]: https://github.com/taiki-e/parse-changelog
-[cargo-release-guide]: ./cargo-release-guide.md
\ No newline at end of file
+[cargo-release-guide]: ./cargo-release-guide.md
diff --git a/cargo-dist-schema/Cargo.toml b/cargo-dist-schema/Cargo.toml
index f5379db3..2b5010f9 100644
--- a/cargo-dist-schema/Cargo.toml
+++ b/cargo-dist-schema/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist-schema"
 description = "Schema information for cargo-dist's dist-manifest.json"
-version = "0.0.5"
+version = "0.0.6-prerelease.2"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml
index 8d39f113..806222a5 100644
--- a/cargo-dist/Cargo.toml
+++ b/cargo-dist/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist"
 description = "Shippable application packaging for Rust"
-version = "0.0.5"
+version = "0.0.6-prerelease.2"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
@@ -30,7 +30,7 @@ clap-cargo = { version = "0.10.0", optional = true }
 axocli = { version = "0.1.0", optional = true }
 
 # Features used by the cli and library
-cargo-dist-schema = { version = "=0.0.5", path = "../cargo-dist-schema" }
+cargo-dist-schema = { version = "=0.0.6-prerelease.2", path = "../cargo-dist-schema" }
 miette = { version = "5.6.0" }
 thiserror = "1.0.35"
 tracing = { version = "0.1.36", features = ["log"] }
diff --git a/cargo-dist/src/ci.rs b/cargo-dist/src/ci.rs
index e0cdb8e4..d1fe1038 100644
--- a/cargo-dist/src/ci.rs
+++ b/cargo-dist/src/ci.rs
@@ -100,7 +100,7 @@ fn write_github_ci<W: std::io::Write>(f: &mut W, dist: &DistGraph) -> Result<(),
     }
 
     // Finally write the final CI script to the Writer
-    let ci_yml = include_str!("ci.yml");
+    let ci_yml = include_str!("../templates/ci.yml");
     let ci_yml = ci_yml
         .replace("{{{{INSTALL_RUST}}}}", &install_rust)
         .replace("{{{{INSTALL_DIST_SH}}}}", &install_dist_sh)
diff --git a/cargo-dist/src/cli.rs b/cargo-dist/src/cli.rs
index 27e0338e..2af5923d 100644
--- a/cargo-dist/src/cli.rs
+++ b/cargo-dist/src/cli.rs
@@ -226,6 +226,8 @@ pub enum InstallerStyle {
     Shell,
     /// Generates a powershell script that fetches/installs the right build
     Powershell,
+    /// Generates an npm project that fetches the right build to your node_modules
+    Npm,
 }
 
 impl InstallerStyle {
@@ -234,6 +236,7 @@ impl InstallerStyle {
         match self {
             InstallerStyle::Shell => cargo_dist::InstallerStyle::Shell,
             InstallerStyle::Powershell => cargo_dist::InstallerStyle::Powershell,
+            InstallerStyle::Npm => cargo_dist::InstallerStyle::Npm,
         }
     }
 }
diff --git a/cargo-dist/src/installer.rs b/cargo-dist/src/installer.rs
index 7f42b732..02d027a3 100644
--- a/cargo-dist/src/installer.rs
+++ b/cargo-dist/src/installer.rs
@@ -2,12 +2,13 @@
 //!
 //! In the future this might get split up into submodules.
 
-use std::fs::File;
+use std::{fs::File, io::BufWriter};
 
+use camino::Utf8Path;
 use miette::{Context, IntoDiagnostic};
 use newline_converter::{dos2unix, unix2dos};
 
-use crate::{DistGraph, InstallerInfo, ZipStyle};
+use crate::{DistGraph, InstallerInfo, NpmInstallerInfo};
 
 ////////////////////////////////////////////////////////////////
 // Shell Installer
@@ -64,7 +65,6 @@ fn write_install_sh_script<W: std::io::Write>(
     for artifact in artifacts {
         assert!(artifact.target_triples.len() == 1, "It's awesome you made multi-arch executable-zips, but now you need to implement support in the sh installer!");
         let target = &artifact.target_triples[0];
-        assert_eq!(artifact.zip_style, ZipStyle::Tar(crate::CompressionImpl::Xzip), "If you're trying to make zip styles configurable, but now you need to implement support in the sh installer!");
         let zip_ext = artifact.zip_style.ext();
         let artifact_name = &artifact.id;
 
@@ -98,7 +98,7 @@ fn write_install_sh_script<W: std::io::Write>(
         }
     }
 
-    let install_script = include_str!("installer.sh");
+    let install_script = include_str!("../templates/installer.sh");
     let install_script = install_script
         .replace("{{APP_NAME}}", app_name)
         .replace("{{APP_VERSION}}", app_version)
@@ -155,7 +155,6 @@ fn write_install_ps_script<W: std::io::Write>(
     for artifact in artifacts {
         assert!(artifact.target_triples.len() == 1, "It's awesome you made multi-arch executable-zips, but now you need to implement support in the ps1 installer!");
         let target = &artifact.target_triples[0];
-        assert_eq!(artifact.zip_style, ZipStyle::Zip, "If you're trying to make zip styles configurable, but now you need to implement support in the ps1 installer!");
         let zip_ext = artifact.zip_style.ext();
         let artifact_name = &artifact.id;
 
@@ -180,7 +179,7 @@ fn write_install_ps_script<W: std::io::Write>(
         entries.push_str(&entry);
     }
     let platform_info = platform_info_template.replace("{{ENTRIES}}", &entries);
-    let install_script = include_str!("installer.ps1");
+    let install_script = include_str!("../templates/installer.ps1");
     let install_script = install_script
         .replace("{{APP_NAME}}", app_name)
         .replace("{{APP_VERSION}}", app_version)
@@ -191,3 +190,153 @@ fn write_install_ps_script<W: std::io::Write>(
 
     Ok(())
 }
+
+////////////////////////////////////////////////////////////////
+// NPM Installer
+////////////////////////////////////////////////////////////////
+
+pub(crate) fn generate_install_npm_project(
+    _dist: &DistGraph,
+    info: &NpmInstallerInfo,
+) -> Result<(), miette::Report> {
+    const TEMPLATE1_NAME: &str = ".gitignore";
+    const TEMPLATE2_NAME: &str = "binary.js";
+    const TEMPLATE3_NAME: &str = "install.js";
+    const TEMPLATE4_NAME: &str = "package-lock.json";
+    const TEMPLATE5_NAME: &str = "package.json";
+    const TEMPLATE6_NAME: &str = "run.js";
+
+    const TEMPLATE1: &str = include_str!("../templates/npm/.gitignore");
+    const TEMPLATE2: &str = include_str!("../templates/npm/binary.js");
+    const TEMPLATE3: &str = include_str!("../templates/npm/install.js");
+    const TEMPLATE4: &str = include_str!("../templates/npm/package-lock.json");
+    const TEMPLATE5: &str = include_str!("../templates/npm/package.json");
+    const TEMPLATE6: &str = include_str!("../templates/npm/run.js");
+
+    let zip_dir = &info.package_dir;
+    apply_npm_templates(TEMPLATE1, zip_dir, TEMPLATE1_NAME, info)?;
+    apply_npm_templates(TEMPLATE2, zip_dir, TEMPLATE2_NAME, info)?;
+    apply_npm_templates(TEMPLATE3, zip_dir, TEMPLATE3_NAME, info)?;
+    apply_npm_templates(TEMPLATE4, zip_dir, TEMPLATE4_NAME, info)?;
+    apply_npm_templates(TEMPLATE5, zip_dir, TEMPLATE5_NAME, info)?;
+    apply_npm_templates(TEMPLATE6, zip_dir, TEMPLATE6_NAME, info)?;
+
+    Ok(())
+}
+
+fn apply_npm_templates(
+    input: &str,
+    target_dir: &Utf8Path,
+    rel_path: &str,
+    info: &NpmInstallerInfo,
+) -> Result<(), miette::Report> {
+    let file_path = target_dir.join(rel_path);
+    let file = File::create(&file_path)
+        .into_diagnostic()
+        .wrap_err_with(|| format!("Failed to create installer file {file_path}"))?;
+    let mut f = BufWriter::new(file);
+
+    // Not yet impl'd
+    let npm_package_keywords = Vec::<String>::new();
+
+    // FIXME: escape these strings!?
+
+    let name = &info.npm_package_name;
+    let version = &info.npm_package_version;
+    let desc = info
+        .npm_package_desc
+        .as_ref()
+        .map(|desc| format!(r#""description": "{desc}","#))
+        .unwrap_or_default();
+    let repository_url = info
+        .npm_package_repository_url
+        .as_ref()
+        .map(|url| format!(r#""repository": "{url}","#))
+        .unwrap_or_default();
+    let homepage_url = info
+        .npm_package_homepage_url
+        .as_ref()
+        .map(|url| format!(r#""homepage": "{url}","#))
+        .unwrap_or_default();
+    let license = info
+        .npm_package_license
+        .as_ref()
+        .map(|license| format!(r#""license": "{license}","#))
+        .unwrap_or_default();
+
+    let authors = match info.npm_package_authors.len() {
+        0 => String::new(),
+        1 => format!(r#""author": "{}","#, info.npm_package_authors[0]),
+        _ => format!(r#""contributors": "{:?}","#, info.npm_package_authors),
+    };
+
+    let keywords = if info.npm_package_authors.is_empty() {
+        String::new()
+    } else {
+        format!(r#""keywords": "{:?}","#, npm_package_keywords)
+    };
+
+    let platform_entry_template = r###"
+  "{{TARGET}}": {
+    "artifact_name": "{{ARTIFACT_NAME}}",
+    "bins": {{BINS}},
+    "zip_ext": "{{ZIP_EXT}}"
+  }"###;
+
+    let mut platform_info = String::new();
+    let last_platform = info.inner.artifacts.len() - 1;
+    for (idx, artifact) in info.inner.artifacts.iter().enumerate() {
+        use std::fmt::Write;
+
+        assert!(artifact.target_triples.len() == 1, "It's awesome you made multi-arch executable-zips, but now you need to implement support in the ps1 installer!");
+        let target = &artifact.target_triples[0];
+        let zip_ext = artifact.zip_style.ext();
+        let artifact_name = &artifact.id;
+
+        let mut bins = String::new();
+        let mut multi_bin = false;
+        bins.push('[');
+        for bin in &artifact.binaries {
+            // FIXME: we should really stop pervasively assuming things are copied to the root...
+            let rel_path = bin;
+            if multi_bin {
+                bins.push_str(", ");
+            } else {
+                multi_bin = true;
+            }
+            write!(bins, "\"{}\"", rel_path).unwrap();
+        }
+        bins.push(']');
+
+        let entry = platform_entry_template
+            .replace("{{TARGET}}", target)
+            .replace("{{ARTIFACT_NAME}}", artifact_name)
+            .replace("{{BINS}}", &bins)
+            .replace("{{ZIP_EXT}}", zip_ext);
+        platform_info.push_str(&entry);
+        if idx != last_platform {
+            platform_info.push(',');
+        }
+    }
+    let output = input
+        .replace("{{PACKAGE_NAME}}", name)
+        .replace("{{PACKAGE_VERSION}}", version)
+        .replace("{{KEY_DESCRIPTION}}", &desc)
+        .replace("{{KEY_REPOSITORY_URL}}", &repository_url)
+        .replace("{{KEY_AUTHORS}}", &authors)
+        .replace("{{KEY_LICENSE}}", &license)
+        .replace("{{KEY_HOMEPAGE_URL}}", &homepage_url)
+        .replace("{{KEY_KEYWORDS}}", &keywords)
+        .replace("/*APP_NAME*/", &info.inner.app_name)
+        .replace("/*ARTIFACT_DOWNLOAD_URL*/", &info.inner.base_url)
+        .replace("/*PLATFORM_INFO*/", &platform_info);
+
+    {
+        use std::io::Write;
+        f.write_all(dos2unix(&output).as_bytes())
+            .into_diagnostic()
+            .wrap_err_with(|| format!("Failed to write to installer file {file_path}"))?;
+    }
+
+    Ok(())
+}
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index 7c88aed9..26e4efca 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -149,27 +149,29 @@ fn manifest_artifact(
             }
         });
 
-    let static_assets = match &artifact.kind {
-        ArtifactKind::ExecutableZip(zip) => zip
-            .static_assets
-            .iter()
-            .map(|(kind, asset)| {
-                let kind = match kind {
-                    StaticAssetKind::Changelog => AssetKind::Changelog,
-                    StaticAssetKind::License => AssetKind::License,
-                    StaticAssetKind::Readme => AssetKind::Readme,
-                    StaticAssetKind::Other => AssetKind::Unknown,
-                };
-                Asset {
-                    name: Some(asset.file_name().unwrap().to_owned()),
-                    path: Some(asset.file_name().unwrap().to_owned()),
-                    kind,
-                }
-            })
-            .collect(),
-        ArtifactKind::Installer(_) => vec![],
-        ArtifactKind::Symbols(_) => vec![],
-    };
+    let static_assets = artifact
+        .archive
+        .as_ref()
+        .map(|archive| {
+            archive
+                .static_assets
+                .iter()
+                .map(|(kind, asset)| {
+                    let kind = match kind {
+                        StaticAssetKind::Changelog => AssetKind::Changelog,
+                        StaticAssetKind::License => AssetKind::License,
+                        StaticAssetKind::Readme => AssetKind::Readme,
+                        StaticAssetKind::Other => AssetKind::Unknown,
+                    };
+                    Asset {
+                        name: Some(asset.file_name().unwrap().to_owned()),
+                        path: Some(asset.file_name().unwrap().to_owned()),
+                        kind,
+                    }
+                })
+                .collect::<Vec<_>>()
+        })
+        .unwrap_or_default();
 
     assets.extend(built_assets);
     assets.extend(static_assets);
@@ -191,7 +193,11 @@ fn manifest_artifact(
             description = None;
             kind = cargo_dist_schema::ArtifactKind::Symbols;
         }
-        ArtifactKind::Installer(InstallerImpl::Powershell(info) | InstallerImpl::Shell(info)) => {
+        ArtifactKind::Installer(
+            InstallerImpl::Powershell(info)
+            | InstallerImpl::Shell(info)
+            | InstallerImpl::Npm(NpmInstallerInfo { inner: info, .. }),
+        ) => {
             install_hint = Some(info.hint.clone());
             description = Some(info.desc.clone());
             kind = cargo_dist_schema::ArtifactKind::Installer;
@@ -403,26 +409,26 @@ fn init_artifact_dir(_dist: &DistGraph, artifact: &Artifact) -> Result<()> {
             .wrap_err_with(|| format!("failed to delete old artifact {}", artifact.file_path))?;
     }
 
-    let Some(artifact_dir_path) = &artifact.dir_path else {
+    let Some(archive) = &artifact.archive else {
         // If there's no dir than we're done
         return Ok(());
     };
-    info!("recreating artifact dir: {artifact_dir_path}");
+    info!("recreating artifact dir: {}", archive.dir_path);
 
     // Clear out the dir we'll build the bundle up in
-    if artifact_dir_path.exists() {
-        std::fs::remove_dir_all(artifact_dir_path)
+    if archive.dir_path.exists() {
+        std::fs::remove_dir_all(&archive.dir_path)
             .into_diagnostic()
-            .wrap_err_with(|| format!("failed to delete old artifact dir {artifact_dir_path}"))?;
+            .wrap_err_with(|| format!("failed to delete old artifact dir {}", archive.dir_path))?;
     }
-    std::fs::create_dir(artifact_dir_path)
+    std::fs::create_dir(&archive.dir_path)
         .into_diagnostic()
-        .wrap_err_with(|| format!("failed to create artifact dir {artifact_dir_path}"))?;
+        .wrap_err_with(|| format!("failed to create artifact dir {}", archive.dir_path))?;
 
     Ok(())
 }
 
-fn copy_file(src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
+pub(crate) fn copy_file(src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
     let _bytes_written = std::fs::copy(src_path, dest_path)
         .into_diagnostic()
         .wrap_err_with(|| format!("failed to copy file {src_path} => {dest_path}"))?;
@@ -695,6 +701,9 @@ fn init_dist_metadata(cfg: &Config, workspace_toml: &mut toml_edit::Document) ->
         dist: None,
         include: vec![],
         auto_includes: None,
+        windows_archive: None,
+        unix_archive: None,
+        npm_scope: None,
     };
 
     const KEY_RUST_VERSION: &str = "rust-toolchain-version";
@@ -726,6 +735,18 @@ fn init_dist_metadata(cfg: &Config, workspace_toml: &mut toml_edit::Document) ->
     const DESC_AUTO_INCLUDE: &str =
         "# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)\n";
 
+    const KEY_WIN_ARCHIVE: &str = "windows-archive";
+    const DESC_WIN_ARCHIVE: &str =
+        "# The archive format to use for windows builds (defaults .zip)\n";
+
+    const KEY_UNIX_ARCHIVE: &str = "unix-archive";
+    const DESC_UNIX_ARCHIVE: &str =
+        "# The archive format to use for non-windows builds (defaults .tar.xz)\n";
+
+    const KEY_NPM_SCOPE: &str = "npm-scope";
+    const DESC_NPM_SCOPE: &str =
+        "# A namespace to use when publishing this package to the npm registry\n";
+
     let mut new_metadata = toml_edit::table();
     let table = new_metadata.as_table_mut().unwrap();
     if let Some(val) = meta.cargo_dist_version {
@@ -764,6 +785,7 @@ fn init_dist_metadata(cfg: &Config, workspace_toml: &mut toml_edit::Document) ->
                     .map(|installer| match installer {
                         InstallerStyle::Powershell => "powershell",
                         InstallerStyle::Shell => "shell",
+                        InstallerStyle::Npm => "npm",
                     })
                     .collect(),
             ),
@@ -801,6 +823,27 @@ fn init_dist_metadata(cfg: &Config, workspace_toml: &mut toml_edit::Document) ->
             .unwrap()
             .set_prefix(DESC_AUTO_INCLUDE);
     }
+    if let Some(val) = meta.windows_archive {
+        table.insert(KEY_WIN_ARCHIVE, value(val.ext()));
+        table
+            .key_decor_mut(KEY_WIN_ARCHIVE)
+            .unwrap()
+            .set_prefix(DESC_WIN_ARCHIVE);
+    }
+    if let Some(val) = meta.unix_archive {
+        table.insert(KEY_UNIX_ARCHIVE, value(val.ext()));
+        table
+            .key_decor_mut(KEY_UNIX_ARCHIVE)
+            .unwrap()
+            .set_prefix(DESC_UNIX_ARCHIVE);
+    }
+    if let Some(val) = meta.npm_scope {
+        table.insert(KEY_NPM_SCOPE, value(val));
+        table
+            .key_decor_mut(KEY_NPM_SCOPE)
+            .unwrap()
+            .set_prefix(DESC_NPM_SCOPE);
+    }
     table
         .decor_mut()
         .set_prefix("\n# Config for 'cargo dist'\n");
@@ -817,9 +860,12 @@ pub struct GenerateCiArgs {}
 /// Generate CI scripts (impl of `cargo dist generate-ci`)
 pub fn do_generate_ci(cfg: &Config, _args: &GenerateCiArgs) -> Result<()> {
     let dist = gather_work(cfg)?;
+    // Enforce cargo-dist-version, unless it's a magic vX.Y.Z-github-BRANCHNAME version,
+    // which we use for testing against a PR branch. In that case the current_version
+    // should be irrelevant (so sayeth the person who made and uses this feature).
     if let Some(desired_version) = &dist.desired_cargo_dist_version {
         let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
-        if desired_version != &current_version {
+        if desired_version != &current_version && !desired_version.pre.starts_with("github-") {
             return Err(miette!("you're running cargo-dist {}, but 'cargo-dist-version = {}' is set in your Cargo.toml\n\nYou should update cargo-dist-version if you want to update to this version", current_version, desired_version));
         }
     }
@@ -842,5 +888,6 @@ fn generate_installer(dist: &DistGraph, style: &InstallerImpl) -> Result<()> {
     match style {
         InstallerImpl::Shell(info) => installer::generate_install_sh_script(dist, info),
         InstallerImpl::Powershell(info) => installer::generate_install_ps_script(dist, info),
+        InstallerImpl::Npm(info) => installer::generate_install_npm_project(dist, info),
     }
 }
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index b3a87dd1..31eb75ff 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -192,6 +192,26 @@ pub struct DistMetadata {
     #[serde(skip_serializing_if = "Option::is_none")]
     #[serde(rename = "auto-includes")]
     pub auto_includes: Option<bool>,
+
+    /// The archive format to use for windows builds (defaults .zip)
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "windows-archive")]
+    pub windows_archive: Option<ZipStyle>,
+
+    /// The archive format to use for non-windows builds (defaults .tar.xz)
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "unix-archive")]
+    pub unix_archive: Option<ZipStyle>,
+
+    /// A scope to prefix npm packages with (@ should be included).
+    ///
+    /// This is required if you're using an npm installer.
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "npm-scope")]
+    pub npm_scope: Option<String>,
 }
 
 impl DistMetadata {
@@ -222,6 +242,15 @@ impl DistMetadata {
         if self.auto_includes.is_none() {
             self.auto_includes = workspace_config.auto_includes;
         }
+        if self.windows_archive.is_none() {
+            self.windows_archive = workspace_config.windows_archive;
+        }
+        if self.unix_archive.is_none() {
+            self.unix_archive = workspace_config.unix_archive;
+        }
+        if self.npm_scope.is_none() {
+            self.npm_scope = workspace_config.npm_scope.clone();
+        }
         self.include
             .extend(workspace_config.include.iter().cloned());
     }
@@ -278,6 +307,9 @@ pub enum InstallerStyle {
     /// Generate a powershell script that fetches from [`DistGraph::artifact_download_url`][]
     #[serde(rename = "powershell")]
     Powershell,
+    /// Generate an npm project that fetches from [`DistGraph::artifact_download_url`][]
+    #[serde(rename = "npm")]
+    Npm,
 }
 
 /// A unique id for a [`Artifact`][]
@@ -396,6 +428,7 @@ pub struct Binary {
 
 /// A build step we would like to perform
 #[derive(Debug)]
+#[allow(clippy::large_enum_variant)]
 pub enum BuildStep {
     /// Do a cargo build (and copy the outputs to various locations)
     Cargo(CargoBuildStep),
@@ -501,17 +534,10 @@ pub struct Artifact {
     ///
     /// i.e. `x86_64-pc-windows-msvc`
     pub target_triples: Vec<TargetTriple>,
-    /// The name of the directory this artifact's contents will be stored in (if necessary).
-    ///
-    /// This directory is technically a transient thing but it will show up as the name of
-    /// the directory in a `tar`. Single file artifacts don't need this.
-    ///
-    /// i.e. `cargo-dist-v0.1.0-x86_64-pc-windows-msvc`
-    pub dir_name: Option<String>,
-    /// The path of the directory this artifact's contents will be stored in (if necessary).
-    ///
-    /// i.e. `/.../target/dist/cargo-dist-v0.1.0-x86_64-pc-windows-msvc/`
-    pub dir_path: Option<Utf8PathBuf>,
+    /// If constructing this artifact involves creating a directory,
+    /// copying static files into it, and then zip/tarring it, set this
+    /// value to automate those tasks.
+    pub archive: Option<Archive>,
     /// The path where the final artifact will appear in the dist dir.
     ///
     /// i.e. `/.../target/dist/cargo-dist-v0.1.0-x86_64-pc-windows-msvc.zip`
@@ -524,8 +550,32 @@ pub struct Artifact {
     pub kind: ArtifactKind,
 }
 
+/// Info about an archive (zip/tarball) that should be made. Currently this is always part
+/// of an Artifact, and the final output will be [`Artifact::file_path`][].
+#[derive(Debug)]
+pub struct Archive {
+    /// The name of the directory this artifact's contents will be stored in.
+    ///
+    /// This directory is technically a transient thing but it will show up as the name of
+    /// the directory in a `tar`.
+    ///
+    /// i.e. `cargo-dist-v0.1.0-x86_64-pc-windows-msvc`
+    pub dir_name: String,
+    /// The path of the directory this artifact's contents will be stored in.
+    ///
+    /// i.e. `/.../target/dist/cargo-dist-v0.1.0-x86_64-pc-windows-msvc/`
+    pub dir_path: Utf8PathBuf,
+    /// The style of zip to make
+    pub zip_style: ZipStyle,
+    /// Static assets to copy to the root of the artifact's dir (path is src)
+    ///
+    /// In the future this might add a custom relative dest path
+    pub static_assets: Vec<(StaticAssetKind, Utf8PathBuf)>,
+}
+
 /// A kind of artifact (more specific fields)
 #[derive(Debug)]
+#[allow(clippy::large_enum_variant)]
 pub enum ArtifactKind {
     /// An executable zip
     ExecutableZip(ExecutableZip),
@@ -538,12 +588,7 @@ pub enum ArtifactKind {
 /// An ExecutableZip Artifact
 #[derive(Debug)]
 pub struct ExecutableZip {
-    /// The style of zip to make
-    pub zip_style: ZipStyle,
-    /// Static assets to copy to the root of the artifact's dir (path is src)
-    ///
-    /// In the future this might add a custom relative dest path
-    pub static_assets: Vec<(StaticAssetKind, Utf8PathBuf)>,
+    // everything important is already part of Artifact
 }
 
 /// A Symbols/Debuginfo Artifact
@@ -558,6 +603,16 @@ pub struct Symbols {
 pub struct Release {
     /// The name of the app
     pub app_name: String,
+    /// A brief description of the app
+    pub app_desc: Option<String>,
+    /// The authors of the app
+    pub app_authors: Vec<String>,
+    /// The license of the app
+    pub app_license: Option<String>,
+    /// The license of the app
+    pub app_repository_url: Option<String>,
+    /// The license of the app
+    pub app_homepage_url: Option<String>,
     /// The version of the app
     pub version: Version,
     /// The unique id of the release (e.g. "my-app-v1.0.0")
@@ -575,6 +630,12 @@ pub struct Release {
     pub changelog_body: Option<String>,
     /// The title of the changelog for this release
     pub changelog_title: Option<String>,
+    /// Archive format to use on windows
+    pub windows_archive: ZipStyle,
+    /// Archive format to use on non-windows
+    pub unix_archive: ZipStyle,
+    /// Static assets that should be included in bundles like executable-zips
+    pub static_assets: Vec<(StaticAssetKind, Utf8PathBuf)>,
 }
 
 /// A particular variant of a Release (e.g. "the macos build")
@@ -594,7 +655,7 @@ pub struct ReleaseVariant {
 }
 
 /// A particular kind of static asset we're interested in
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
 pub enum StaticAssetKind {
     /// A README file
     Readme,
@@ -639,6 +700,35 @@ impl ZipStyle {
     }
 }
 
+impl Serialize for ZipStyle {
+    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.ext())
+    }
+}
+
+impl<'de> Deserialize<'de> for ZipStyle {
+    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use serde::de::Error;
+
+        let ext = String::deserialize(deserializer)?;
+        match &*ext {
+            ".zip" => Ok(ZipStyle::Zip),
+            ".tar.gz" => Ok(ZipStyle::Tar(CompressionImpl::Gzip)),
+            ".tar.xz" => Ok(ZipStyle::Tar(CompressionImpl::Xzip)),
+            ".tar.zstd" => Ok(ZipStyle::Tar(CompressionImpl::Zstd)),
+            _ => Err(D::Error::custom(format!(
+                "unknown archive format {ext}, expected one of: .zip, .tar.gz, .tar.xz, .tar.zstd"
+            ))),
+        }
+    }
+}
+
 /// A kind of an installer
 #[derive(Debug, Clone)]
 pub enum InstallerImpl {
@@ -646,6 +736,8 @@ pub enum InstallerImpl {
     Shell(InstallerInfo),
     /// powershell installer script
     Powershell(InstallerInfo),
+    /// npm installer package
+    Npm(NpmInstallerInfo),
 }
 
 /// Generic info about an installer
@@ -667,6 +759,29 @@ pub struct InstallerInfo {
     pub hint: String,
 }
 
+/// Info about an npm installer
+#[derive(Debug, Clone)]
+pub struct NpmInstallerInfo {
+    /// The name of the npm package
+    pub npm_package_name: String,
+    /// The version of the npm package
+    pub npm_package_version: String,
+    /// Short description of the package
+    pub npm_package_desc: Option<String>,
+    /// URL to repository
+    pub npm_package_repository_url: Option<String>,
+    /// URL to homepage
+    pub npm_package_homepage_url: Option<String>,
+    /// Short description of the package
+    pub npm_package_authors: Vec<String>,
+    /// Short description of the package
+    pub npm_package_license: Option<String>,
+    /// Generic installer info
+    pub inner: InstallerInfo,
+    /// Dir to build the package in
+    pub package_dir: Utf8PathBuf,
+}
+
 /// A fake fragment of an ExecutableZip artifact for installers
 #[derive(Debug, Clone)]
 pub struct ExecutableZipFragment {
@@ -793,12 +908,51 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         self.inner.ci_style = style;
     }
 
-    fn add_release(&mut self, app_name: String, version: Version) -> ReleaseIdx {
+    fn add_release(&mut self, pkg_idx: PackageIdx) -> ReleaseIdx {
+        let package_info = self.workspace().package(pkg_idx);
+        let package_config = self.package_metadata(pkg_idx);
+
+        let version = package_info.version.as_ref().unwrap().cargo().clone();
+        let app_name = package_info.name.clone();
+        let app_desc = package_info.description.clone();
+        let app_authors = package_info.authors.clone();
+        let app_license = package_info.license.clone();
+        let app_repository_url = package_info.repository_url.clone();
+        let app_homepage_url = package_info.homepage_url.clone();
+
+        let windows_archive = package_config.windows_archive.unwrap_or(ZipStyle::Zip);
+        let unix_archive = package_config
+            .unix_archive
+            .unwrap_or(ZipStyle::Tar(CompressionImpl::Xzip));
+
+        // Add static assets
+        let mut static_assets = vec![];
+        let auto_includes_enabled = package_config.auto_includes.unwrap_or(true);
+        if auto_includes_enabled {
+            if let Some(readme) = &package_info.readme_file {
+                static_assets.push((StaticAssetKind::Readme, readme.clone()));
+            }
+            if let Some(changelog) = &package_info.changelog_file {
+                static_assets.push((StaticAssetKind::Changelog, changelog.clone()));
+            }
+            for license in &package_info.license_files {
+                static_assets.push((StaticAssetKind::License, license.clone()));
+            }
+        }
+        for static_asset in &package_config.include {
+            static_assets.push((StaticAssetKind::Other, static_asset.clone()));
+        }
+
         let idx = ReleaseIdx(self.inner.releases.len());
         let id = format!("{app_name}-v{version}");
         info!("added release {id}");
         self.inner.releases.push(Release {
             app_name,
+            app_desc,
+            app_authors,
+            app_license,
+            app_repository_url,
+            app_homepage_url,
             version,
             id,
             global_artifacts: vec![],
@@ -806,6 +960,9 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             variants: vec![],
             changelog_body: None,
             changelog_title: None,
+            windows_archive,
+            unix_archive,
+            static_assets,
         });
         idx
     }
@@ -816,8 +973,10 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             id: release_id,
             variants,
             targets,
+            static_assets,
             ..
         } = self.release_mut(to_release);
+        let static_assets = static_assets.clone();
         let id = format!("{release_id}-{target}");
         info!("added variant {id}");
 
@@ -829,7 +988,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             id,
             local_artifacts: vec![],
             binaries: vec![],
-            static_assets: vec![],
+            static_assets,
         });
         idx
     }
@@ -869,16 +1028,6 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         self.variant_mut(to_variant).binaries.push(idx);
     }
 
-    fn add_static_asset(
-        &mut self,
-        to_variant: ReleaseVariantIdx,
-        kind: StaticAssetKind,
-        path: Utf8PathBuf,
-    ) {
-        let ReleaseVariant { static_assets, .. } = self.variant_mut(to_variant);
-        static_assets.push((kind, path));
-    }
-
     fn add_executable_zip(&mut self, to_release: ReleaseIdx) {
         if !self.local_artifacts_enabled() {
             return;
@@ -892,7 +1041,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         let release = self.release(to_release);
         let variants = release.variants.clone();
         for variant_idx in variants {
-            let (zip_artifact, built_assets) = self.make_executable_zip_for_variant(variant_idx);
+            let (zip_artifact, built_assets) =
+                self.make_executable_zip_for_variant(to_release, variant_idx);
 
             let zip_artifact_idx = self.add_local_artifact(variant_idx, zip_artifact);
             for (binary, dest_path) in built_assets {
@@ -906,20 +1056,19 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
     /// This is useful for installers which want to know about *potential* executable zips
     fn make_executable_zip_for_variant(
         &self,
+        release_idx: ReleaseIdx,
         variant_idx: ReleaseVariantIdx,
     ) -> (Artifact, Vec<(BinaryIdx, Utf8PathBuf)>) {
         // This is largely just a lot of path/name computation
         let dist_dir = &self.inner.dist_dir;
+        let release = self.release(release_idx);
         let variant = self.variant(variant_idx);
 
-        // FIXME: this should be configurable
         let target_is_windows = variant.target.contains("windows");
         let zip_style = if target_is_windows {
-            // Windows loves them zips
-            ZipStyle::Zip
+            release.windows_archive
         } else {
-            // tar.xz is well-supported everywhere and much better than tar.gz
-            ZipStyle::Tar(CompressionImpl::Xzip)
+            release.unix_archive
         };
         let platform_exe_ext = if target_is_windows { ".exe" } else { "" };
 
@@ -942,14 +1091,15 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             Artifact {
                 id: artifact_name,
                 target_triples: vec![variant.target.clone()],
-                dir_name: Some(artifact_dir_name),
-                dir_path: Some(artifact_dir_path),
                 file_path: artifact_path,
                 required_binaries: FastMap::new(),
-                kind: ArtifactKind::ExecutableZip(ExecutableZip {
+                archive: Some(Archive {
+                    dir_name: artifact_dir_name,
+                    dir_path: artifact_dir_path,
                     zip_style,
                     static_assets,
                 }),
+                kind: ArtifactKind::ExecutableZip(ExecutableZip {}),
             },
             built_assets,
         )
@@ -990,8 +1140,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 let artifact = Artifact {
                     id: dest_symbol_name,
                     target_triples: vec![binary.target.clone()],
-                    dir_name: None,
-                    dir_path: None,
+                    archive: None,
                     file_path: artifact_path.clone(),
                     required_binaries: FastMap::new(),
                     kind: ArtifactKind::Symbols(Symbols { kind: symbol_kind }),
@@ -1022,6 +1171,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         match installer {
             InstallerStyle::Shell => self.add_shell_installer(to_release),
             InstallerStyle::Powershell => self.add_powershell_installer(to_release),
+            InstallerStyle::Npm => self.add_npm_installer(to_release),
         }
     }
 
@@ -1053,15 +1203,13 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             // Compute the artifact zip this variant *would* make *if* it were built
             // FIXME: this is a kind of hacky workaround for the fact that we don't have a good
             // way to add artifacts to the graph and then say "ok but don't build it".
-            let (artifact, binaries) = self.make_executable_zip_for_variant(variant_idx);
-            let ArtifactKind::ExecutableZip(zip) = artifact.kind else {
-                unreachable!();
-            };
+            let (artifact, binaries) =
+                self.make_executable_zip_for_variant(to_release, variant_idx);
             target_triples.insert(target.clone());
             artifacts.push(ExecutableZipFragment {
                 id: artifact.id,
                 target_triples: artifact.target_triples,
-                zip_style: zip.zip_style,
+                zip_style: artifact.archive.as_ref().unwrap().zip_style,
                 binaries: binaries
                     .into_iter()
                     .map(|(_, dest_path)| dest_path.file_name().unwrap().to_owned())
@@ -1076,8 +1224,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         let installer_artifact = Artifact {
             id: artifact_name,
             target_triples: target_triples.into_iter().collect(),
-            dir_name: None,
-            dir_path: None,
+            archive: None,
             file_path: artifact_path.clone(),
             required_binaries: FastMap::new(),
             kind: ArtifactKind::Installer(InstallerImpl::Shell(InstallerInfo {
@@ -1124,15 +1271,13 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             // Compute the artifact zip this variant *would* make *if* it were built
             // FIXME: this is a kind of hacky workaround for the fact that we don't have a good
             // way to add artifacts to the graph and then say "ok but don't build it".
-            let (artifact, binaries) = self.make_executable_zip_for_variant(variant_idx);
-            let ArtifactKind::ExecutableZip(zip) = artifact.kind else {
-                unreachable!();
-            };
+            let (artifact, binaries) =
+                self.make_executable_zip_for_variant(to_release, variant_idx);
             target_triples.insert(target.clone());
             artifacts.push(ExecutableZipFragment {
                 id: artifact.id,
                 target_triples: artifact.target_triples,
-                zip_style: zip.zip_style,
+                zip_style: artifact.archive.as_ref().unwrap().zip_style,
                 binaries: binaries
                     .into_iter()
                     .map(|(_, dest_path)| dest_path.file_name().unwrap().to_owned())
@@ -1147,10 +1292,9 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         let installer_artifact = Artifact {
             id: artifact_name,
             target_triples: target_triples.into_iter().collect(),
-            dir_name: None,
-            dir_path: None,
             file_path: artifact_path.clone(),
             required_binaries: FastMap::new(),
+            archive: None,
             kind: ArtifactKind::Installer(InstallerImpl::Powershell(InstallerInfo {
                 dest_path: artifact_path,
                 app_name: release.app_name.clone(),
@@ -1165,6 +1309,113 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         self.add_global_artifact(to_release, installer_artifact);
     }
 
+    fn add_npm_installer(&mut self, to_release: ReleaseIdx) {
+        if !self.global_artifacts_enabled() {
+            return;
+        }
+        let release = self.release(to_release);
+        let release_id = &release.id;
+        let Some(download_url) = &self.inner.artifact_download_url else {
+            warn!("skipping npm installer: couldn't compute a URL to download artifacts from");
+            return;
+        };
+
+        let npm_package_name = if let Some(scope) = &self.workspace_metadata.npm_scope {
+            format!("{scope}/{}", release.app_name)
+        } else {
+            release.app_name.clone()
+        };
+        let npm_package_version = release.version.to_string();
+        let npm_package_desc = release.app_desc.clone();
+        let npm_package_authors = release.app_authors.clone();
+        let npm_package_license = release.app_license.clone();
+        let npm_package_repository_url = release.app_repository_url.clone();
+        let npm_package_homepage_url = release.app_homepage_url.clone();
+
+        let static_assets = release.static_assets.clone();
+        let dir_name = format!("{release_id}-npm-package");
+        let dir_path = self.inner.dist_dir.join(&dir_name);
+        let zip_style = ZipStyle::Tar(CompressionImpl::Gzip);
+        let zip_ext = zip_style.ext();
+        let artifact_name = format!("{dir_name}{zip_ext}");
+        let artifact_path = self.inner.dist_dir.join(&artifact_name);
+        // let installer_url = format!("{download_url}/{artifact_name}");
+        let hint = format!("npm install {npm_package_name}@{npm_package_version}");
+        let desc = "Install prebuilt binaries into your npm project".to_owned();
+
+        // Gather up the bundles the installer supports
+        let mut artifacts = vec![];
+        let mut target_triples = SortedSet::new();
+        let mut has_sketchy_archives = false;
+        for &variant_idx in &release.variants {
+            let variant = self.variant(variant_idx);
+            let target = &variant.target;
+            // Compute the artifact zip this variant *would* make *if* it were built
+            // FIXME: this is a kind of hacky workaround for the fact that we don't have a good
+            // way to add artifacts to the graph and then say "ok but don't build it".
+            let (artifact, binaries) =
+                self.make_executable_zip_for_variant(to_release, variant_idx);
+            target_triples.insert(target.clone());
+
+            let variant_zip_style = artifact.archive.as_ref().unwrap().zip_style;
+            if variant_zip_style != ZipStyle::Tar(CompressionImpl::Gzip) {
+                has_sketchy_archives = true;
+            }
+
+            artifacts.push(ExecutableZipFragment {
+                id: artifact.id,
+                target_triples: artifact.target_triples,
+                zip_style: variant_zip_style,
+                binaries: binaries
+                    .into_iter()
+                    .map(|(_, dest_path)| dest_path.file_name().unwrap().to_owned())
+                    .collect(),
+            });
+        }
+
+        if has_sketchy_archives {
+            warn!("the npm installer currently only knows how to unpack .tar.gz archives\n  consider setting windows-archive and unix-archive to .tar.gz in your config");
+        }
+        if artifacts.is_empty() {
+            warn!("skipping npm installer: not building any supported platforms (use --artifacts=global)");
+            return;
+        };
+
+        let installer_artifact = Artifact {
+            id: artifact_name,
+            target_triples: target_triples.into_iter().collect(),
+            archive: Some(Archive {
+                dir_name,
+                dir_path: dir_path.clone(),
+                zip_style,
+                static_assets,
+            }),
+            file_path: artifact_path.clone(),
+            required_binaries: FastMap::new(),
+            kind: ArtifactKind::Installer(InstallerImpl::Npm(NpmInstallerInfo {
+                npm_package_name,
+                npm_package_version,
+                npm_package_desc,
+                npm_package_authors,
+                npm_package_license,
+                npm_package_repository_url,
+                npm_package_homepage_url,
+                package_dir: dir_path,
+                inner: InstallerInfo {
+                    dest_path: artifact_path,
+                    app_name: release.app_name.clone(),
+                    app_version: release.version.to_string(),
+                    base_url: download_url.clone(),
+                    artifacts,
+                    hint,
+                    desc,
+                },
+            })),
+        };
+
+        self.add_global_artifact(to_release, installer_artifact);
+    }
+
     fn add_local_artifact(
         &mut self,
         to_variant: ReleaseVariantIdx,
@@ -1204,31 +1455,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
 
         for artifact in &self.inner.artifacts {
             match &artifact.kind {
-                ArtifactKind::ExecutableZip(zip) => {
-                    let artifact_dir = artifact.dir_path.as_ref().unwrap();
-                    // Copy all the static assets
-                    for (_, src_path) in &zip.static_assets {
-                        let src_path = src_path.clone();
-                        let file_name = src_path.file_name().unwrap();
-                        let dest_path = artifact_dir.join(file_name);
-                        if src_path.is_dir() {
-                            build_steps.push(BuildStep::CopyDir(CopyDirStep {
-                                src_path,
-                                dest_path,
-                            }))
-                        } else {
-                            build_steps.push(BuildStep::CopyFile(CopyFileStep {
-                                src_path,
-                                dest_path,
-                            }))
-                        }
-                    }
-                    // Zip up the artifact
-                    build_steps.push(BuildStep::Zip(ZipDirStep {
-                        src_path: artifact_dir.to_owned(),
-                        dest_path: artifact.file_path.clone(),
-                        zip_style: zip.zip_style,
-                    }));
+                ArtifactKind::ExecutableZip(_zip) => {
+                    // compute_cargo_builds and artifact.archive handle everything
                 }
                 ArtifactKind::Symbols(symbols) => {
                     match symbols.kind {
@@ -1248,6 +1476,33 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                     build_steps.push(BuildStep::GenerateInstaller(installer.clone()));
                 }
             }
+
+            if let Some(archive) = &artifact.archive {
+                let artifact_dir = &archive.dir_path;
+                // Copy all the static assets
+                for (_, src_path) in &archive.static_assets {
+                    let src_path = src_path.clone();
+                    let file_name = src_path.file_name().unwrap();
+                    let dest_path = artifact_dir.join(file_name);
+                    if src_path.is_dir() {
+                        build_steps.push(BuildStep::CopyDir(CopyDirStep {
+                            src_path,
+                            dest_path,
+                        }))
+                    } else {
+                        build_steps.push(BuildStep::CopyFile(CopyFileStep {
+                            src_path,
+                            dest_path,
+                        }))
+                    }
+                }
+                // Zip up the artifact
+                build_steps.push(BuildStep::Zip(ZipDirStep {
+                    src_path: artifact_dir.to_owned(),
+                    dest_path: artifact.file_path.clone(),
+                    zip_style: archive.zip_style,
+                }));
+            }
         }
 
         self.inner.build_steps = build_steps;
@@ -1441,7 +1696,9 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             if !global_installers.is_empty() {
                 writeln!(gh_body, "## Install {heading_suffix}\n").unwrap();
                 for (_installer, details) in global_installers {
-                    let (InstallerImpl::Shell(info) | InstallerImpl::Powershell(info)) = details;
+                    let (InstallerImpl::Shell(info)
+                    | InstallerImpl::Powershell(info)
+                    | InstallerImpl::Npm(NpmInstallerInfo { inner: info, .. })) = details;
 
                     writeln!(&mut gh_body, "### {}\n", info.desc).unwrap();
                     writeln!(&mut gh_body, "```shell\n{}\n```\n", info.hint).unwrap();
@@ -1795,15 +2052,11 @@ pub fn gather_work(cfg: &Config) -> Result<DistGraph> {
 
     // Create a Release for each package
     for (pkg_idx, binaries) in &rust_releases {
-        // FIXME: make app name configurable? Use some other fields in the PackageMetadata?
-        let package_info = graph.workspace().package(*pkg_idx);
         // FIXME: this clone is hacky but I'm in the middle of a nasty refactor
         let package_config = graph.package_metadata(*pkg_idx).clone();
-        let app_name = package_info.name.clone();
-        let version = package_info.version.as_ref().unwrap().cargo().clone();
 
         // Create a Release for this binary
-        let release = graph.add_release(app_name, version);
+        let release = graph.add_release(*pkg_idx);
 
         // Create variants for this Release for each target
         for target in triples {
@@ -1825,23 +2078,6 @@ pub fn gather_work(cfg: &Config) -> Result<DistGraph> {
             for binary in binaries {
                 graph.add_binary(variant, *pkg_idx, (*binary).clone());
             }
-
-            // Add static assets
-            let auto_includes_enabled = package_config.auto_includes.unwrap_or(true);
-            if auto_includes_enabled {
-                if let Some(readme) = &package_info.readme_file {
-                    graph.add_static_asset(variant, StaticAssetKind::Readme, readme.clone());
-                }
-                if let Some(changelog) = &package_info.changelog_file {
-                    graph.add_static_asset(variant, StaticAssetKind::Changelog, changelog.clone());
-                }
-                for license in &package_info.license_files {
-                    graph.add_static_asset(variant, StaticAssetKind::License, license.clone());
-                }
-            }
-            for static_asset in &package_config.include {
-                graph.add_static_asset(variant, StaticAssetKind::Other, static_asset.clone())
-            }
         }
         // Add executable zips to the Release
         graph.add_executable_zip(release);
diff --git a/cargo-dist/src/ci.yml b/cargo-dist/templates/ci.yml
similarity index 100%
rename from cargo-dist/src/ci.yml
rename to cargo-dist/templates/ci.yml
diff --git a/cargo-dist/src/installer.ps1 b/cargo-dist/templates/installer.ps1
similarity index 91%
rename from cargo-dist/src/installer.ps1
rename to cargo-dist/templates/installer.ps1
index b0255129..6d165992 100644
--- a/cargo-dist/src/installer.ps1
+++ b/cargo-dist/templates/installer.ps1
@@ -104,11 +104,26 @@ function Download($download_url, $platforms) {
   $wc = New-Object Net.Webclient
   $wc.downloadFile($url, $dir_path)
 
-  # FIXME: this will probably need to do something else for a zip_ext != ?
-  # at worst we can just stop here and the file is fetched for the user?
   "Unpacking to $tmp" | Out-Host
-  Expand-Archive -Path $dir_path -DestinationPath "$tmp"
-  
+
+  # Select the tool to unpack the files with.
+  #
+  # As of windows 10(?), powershell comes with tar preinstalled, but in practice
+  # it only seems to support .tar.gz, and not xz/zstd. Still, we should try to
+  # forward all tars to it in case the user has a machine that can handle it!
+  switch -Wildcard ($zip_ext) {
+    ".zip" {
+      Expand-Archive -Path $dir_path -DestinationPath "$tmp";
+      Break
+    }
+    ".tar.*" {
+      tar xf $dir_path --strip-components 1 -C "$tmp";
+      Break
+    }
+    Default {
+      throw "ERROR: unknown archive format $zip_ext"
+    }
+  }
 
   # Let the next step know what to copy
   $bin_paths = @()
diff --git a/cargo-dist/src/installer.sh b/cargo-dist/templates/installer.sh
similarity index 91%
rename from cargo-dist/src/installer.sh
rename to cargo-dist/templates/installer.sh
index 0b23c5a2..ba856b11 100644
--- a/cargo-dist/src/installer.sh
+++ b/cargo-dist/templates/installer.sh
@@ -42,7 +42,8 @@ download_binary_and_run_installer() {
     local _zip_ext=""
     local _artifact_name=""
 
-    # This """lookup table""" is generated by cargo-dist 
+    # This """lookup table""" is generated by cargo-dist
+    # and populates the above locals with the right values
     case "$_arch" in {{PLATFORM_INFO}}
         *)
             err "there isn't a package for $_arch"
@@ -69,9 +70,21 @@ download_binary_and_run_installer() {
       exit 1
     fi
 
-    # unpack the zip
-    say "unpacking" 1>&2
-    ensure tar xf "$_file" --strip-components 1 -C "$_dir"
+    # unpack the archive
+    say "unpacking to $_dir" 1>&2
+    case "$_zip_ext" in
+        ".zip")
+            ensure unzip -q "$_file" -d "$_dir"
+            ;;
+
+        ".tar."*)
+            ensure tar xf "$_file" --strip-components 1 -C "$_dir"
+            ;;
+        *)
+            err "unknown archive format: $_zip_ext"
+            ;;
+    esac
+
     for _bin_name in $_bins; do
         say "  unpacked $_bin_name"
     done
@@ -97,6 +110,8 @@ install() {
     for _bin_name in $_bins; do
         local _bin="$_src_dir/$_bin_name"
         cp "$_bin" "$_install_dir"
+        # unzip seems to need this chmod
+        chmod +x "$_install_dir/$_bin_name"
     done
     
     say "everything's installed!"
diff --git a/cargo-dist/templates/npm/.gitignore b/cargo-dist/templates/npm/.gitignore
new file mode 100644
index 00000000..5a643225
--- /dev/null
+++ b/cargo-dist/templates/npm/.gitignore
@@ -0,0 +1,3 @@
+/node_modules
+README.md
+LICENSE
diff --git a/cargo-dist/templates/npm/binary.js b/cargo-dist/templates/npm/binary.js
new file mode 100644
index 00000000..64bffa19
--- /dev/null
+++ b/cargo-dist/templates/npm/binary.js
@@ -0,0 +1,115 @@
+const { Binary } = require("binary-install");
+const os = require("os");
+const cTable = require("console.table");
+const libc = require("detect-libc");
+const { configureProxy } = require("axios-proxy-builder");
+
+const error = (msg) => {
+  console.error(msg);
+  process.exit(1);
+};
+
+const { version } = require("./package.json");
+// These are injected by cargo-dist
+const name = "/*APP_NAME*/";
+const artifact_download_url = "/*ARTIFACT_DOWNLOAD_URL*/"
+
+// These are injected by cargo-dist
+const supportedPlatforms = {/*PLATFORM_INFO*/};
+
+const getPlatform = () => {
+  const raw_os_type = os.type();
+  const raw_architecture = os.arch();
+
+  // We want to use rust-style target triples as the canonical key
+  // for a platform, so translate the "os" library's concepts into rust ones
+  let os_type = "";
+  switch (raw_os_type) {
+    case "Windows_NT": 
+      os_type = "pc-windows-msvc";
+      break;
+    case "Darwin":
+      os_type = "apple-darwin";
+      break;
+    case "Linux":
+      os_type = "unknown-linux-gnu"
+      break;
+  }
+
+  let arch = "";
+  switch (raw_architecture) {
+    case "x64":
+      arch = "x86_64";
+      break;
+    case "arm64":
+      arch = "aarch64";
+      break;
+  }
+
+  // Assume the above succeeded and build a target triple to look things up with.
+  // If any of it failed, this lookup will fail and we'll handle it like normal.
+  let target_triple = `${arch}-${os_type}`;
+  let platform = supportedPlatforms[target_triple];
+
+  if (!platform) {
+    error(
+      `Platform with type "${raw_os_type}" and architecture "${raw_architecture}" is not supported by ${name}.\nYour system must be one of the following:\n\n${Object.keys(supportedPlatforms).join(",")}`
+    );
+  }
+
+  // These are both situation where you might toggle to unknown-linux-musl but we don't support that yet
+  if (raw_os_type === "Linux") {
+    if (libc.isNonGlibcLinuxSync()) {
+      error("This operating system does not support dynamic linking to glibc.");
+    } else {
+      let libc_version = libc.versionSync();
+      let split_libc_version = libc_version.split(".");
+      let libc_major_version = split_libc_version[0];
+      let libc_minor_version = split_libc_version[1];
+      let min_major_version = 2;
+      let min_minor_version = 17;
+      if (
+        libc_major_version < min_major_version ||
+        libc_minor_version < min_minor_version
+      ) {
+        error(
+          `This operating system needs glibc >= ${min_major_version}.${min_minor_version}, but only has ${libc_version} installed.`
+        );
+      }
+    }
+  }
+
+  return platform;
+};
+
+const getBinary = () => {
+  const platform = getPlatform();
+  const url = `${artifact_download_url}/${platform.artifact_name}`;
+
+  if (platform.bins.length > 1) {
+    // Not yet supported
+    error("this app has multiple binaries, which isn't yet implemented");
+  }
+  let binary = new Binary(platform.bins[0], url);
+
+  return binary;
+};
+
+const install = () => {
+  const binary = getBinary();
+  const proxy = configureProxy(binary.url);
+  const suppressLogs = false;
+
+  return binary.install(proxy, suppressLogs);
+};
+
+const run = () => {
+  const binary = getBinary();
+  binary.run();
+};
+
+module.exports = {
+  install,
+  run,
+  getBinary,
+};
diff --git a/cargo-dist/templates/npm/install.js b/cargo-dist/templates/npm/install.js
new file mode 100644
index 00000000..9b5bba1b
--- /dev/null
+++ b/cargo-dist/templates/npm/install.js
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+
+const { install } = require("./binary");
+install();
diff --git a/cargo-dist/templates/npm/package-lock.json b/cargo-dist/templates/npm/package-lock.json
new file mode 100644
index 00000000..f3868bf8
--- /dev/null
+++ b/cargo-dist/templates/npm/package-lock.json
@@ -0,0 +1,354 @@
+{
+  "name": "{{PACKAGE_NAME}}",
+  "version": "{{PACKAGE_VERSION}}",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "{{PACKAGE_NAME}}",
+      "version": "{{PACKAGE_VERSION}}",
+      {{KEY_LICENSE}}
+      "hasInstallScript": true,
+      "dependencies": {
+        "axios-proxy-builder": "^0.1.1",
+        "binary-install": "^1.0.6",
+        "console.table": "^0.10.0",
+        "detect-libc": "^2.0.0"
+      },
+      "bin": {
+        "rover": "run.js"
+      },
+      "devDependencies": {
+        "prettier": "2.8.4"
+      },
+      "engines": {
+        "node": ">=14",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/axios": {
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+      "dependencies": {
+        "follow-redirects": "^1.14.8"
+      }
+    },
+    "node_modules/axios-proxy-builder": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/axios-proxy-builder/-/axios-proxy-builder-0.1.2.tgz",
+      "integrity": "sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==",
+      "dependencies": {
+        "tunnel": "^0.0.6"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/binary-install": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.0.6.tgz",
+      "integrity": "sha512-h3K4jaC4jEauK3csXI9GxGBJldkpuJlHCIBv8i+XBNhPuxnlERnD1PWVczQYDqvhJfv0IHUbB3lhDrZUMHvSgw==",
+      "dependencies": {
+        "axios": "^0.26.1",
+        "rimraf": "^3.0.2",
+        "tar": "^6.1.11"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+      "optional": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "node_modules/console.table": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz",
+      "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==",
+      "dependencies": {
+        "easy-table": "1.1.0"
+      },
+      "engines": {
+        "node": "> 0.10"
+      }
+    },
+    "node_modules/defaults": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+      "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+      "optional": true,
+      "dependencies": {
+        "clone": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
+      "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/easy-table": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
+      "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==",
+      "optionalDependencies": {
+        "wcwidth": ">=1.0.1"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.4.tgz",
+      "integrity": "sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
+      "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/tar": {
+      "version": "6.1.13",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
+      "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^4.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/tunnel": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+      "engines": {
+        "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+      }
+    },
+    "node_modules/wcwidth": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+      "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+      "optional": true,
+      "dependencies": {
+        "defaults": "^1.0.3"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    }
+  }
+}
diff --git a/cargo-dist/templates/npm/package.json b/cargo-dist/templates/npm/package.json
new file mode 100644
index 00000000..81604969
--- /dev/null
+++ b/cargo-dist/templates/npm/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "{{PACKAGE_NAME}}",
+  "version": "{{PACKAGE_VERSION}}",
+  {{KEY_DESCRIPTION}}
+  {{KEY_REPOSITORY_URL}}
+  {{KEY_HOMEPAGE_URL}}
+  {{KEY_AUTHORS}}
+  {{KEY_LICENSE}}
+  {{KEY_KEYWORDS}}
+  "main": "index.js",
+  "bin": {
+    "rover": "run.js"
+  },
+  "scripts": {
+    "postinstall": "node ./install.js",
+    "fmt": "prettier --write **/*.js",
+    "fmt:check": "prettier --check **/*.js"
+  },
+  "engines": {
+    "node": ">=14",
+    "npm": ">=6"
+  },
+  "volta": {
+    "node": "18.14.1",
+    "npm": "9.5.0"
+  },
+  "dependencies": {
+    "axios-proxy-builder": "^0.1.1",
+    "binary-install": "^1.0.6",
+    "console.table": "^0.10.0",
+    "detect-libc": "^2.0.0"
+  },
+  "devDependencies": {
+    "prettier": "2.8.4"
+  }
+}
diff --git a/cargo-dist/templates/npm/run.js b/cargo-dist/templates/npm/run.js
new file mode 100644
index 00000000..c36608b1
--- /dev/null
+++ b/cargo-dist/templates/npm/run.js
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+
+const { run, install: maybeInstall } = require("./binary");
+maybeInstall(true).then(run);
\ No newline at end of file


Test patch:
diff --git a/cargo-dist/tests/snapshots/cli_tests__long-help.snap b/cargo-dist/tests/snapshots/cli_tests__long-help.snap
index 431be658..28b87ad6 100644
--- a/cargo-dist/tests/snapshots/cli_tests__long-help.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__long-help.snap
@@ -81,6 +81,7 @@ GLOBAL OPTIONS:
           Possible values:
           - shell:      Generates a shell script that fetches/installs the right build
           - powershell: Generates a powershell script that fetches/installs the right build
+          - npm:        Generates an npm project that fetches the right build to your node_modules
 
       --ci <CI>
           CI we want to support
diff --git a/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap b/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap
index 66d20e00..7e78d048 100644
--- a/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap
@@ -61,6 +61,7 @@ If left unspecified we will use the values in [workspace.metadata.dist]. `cargo
 Possible values:
 - shell:      Generates a shell script that fetches/installs the right build
 - powershell: Generates a powershell script that fetches/installs the right build
+- npm:        Generates an npm project that fetches the right build to your node_modules
 
 #### `--ci <CI>`
 CI we want to support
diff --git a/cargo-dist/tests/snapshots/cli_tests__short-help.snap b/cargo-dist/tests/snapshots/cli_tests__short-help.snap
index d027143c..af069161 100644
--- a/cargo-dist/tests/snapshots/cli_tests__short-help.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__short-help.snap
@@ -25,7 +25,7 @@ GLOBAL OPTIONS:
       --output-format <OUTPUT_FORMAT>  The format of the output [default: human] [possible values: human, json]
       --no-local-paths                 Strip local paths from output (e.g. in the dist manifest json)
       --target <TARGET>                Target triples we want to build
-      --installer <INSTALLER>          Installers we want to build [possible values: shell, powershell]
+      --installer <INSTALLER>          Installers we want to build [possible values: shell, powershell, npm]
       --ci <CI>                        CI we want to support [possible values: github]
       --tag <TAG>                      The (git) tag to use for the Announcement that each invocation of cargo-dist is performing