diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json
index 5c056463e08..a7a1829dd5c 100644
--- a/editors/code/package-lock.json
+++ b/editors/code/package-lock.json
@@ -1,6 +1,6 @@
 {
     "name": "rust-analyzer",
-    "version": "0.1.0",
+    "version": "0.2.0-dev",
     "lockfileVersion": 1,
     "requires": true,
     "dependencies": {
diff --git a/editors/code/package.json b/editors/code/package.json
index f687eb8d458..2b8e5aec5e3 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -5,7 +5,8 @@
     "preview": true,
     "private": true,
     "icon": "icon.png",
-    "version": "0.1.0",
+    "//": "The real version is in release.yaml, this one just needs to be bigger",
+    "version": "0.2.0-dev",
     "publisher": "matklad",
     "repository": {
         "url": "https://github.com/rust-analyzer/rust-analyzer.git",
diff --git a/xtask/src/cmd.rs b/xtask/src/cmd.rs
deleted file mode 100644
index 37497fb7458..00000000000
--- a/xtask/src/cmd.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-use std::process::{Command, Output, Stdio};
-
-use anyhow::{Context, Result};
-
-use crate::project_root;
-
-pub struct Cmd<'a> {
-    pub unix: &'a str,
-    pub windows: &'a str,
-    pub work_dir: &'a str,
-}
-
-impl Cmd<'_> {
-    pub fn run(self) -> Result<()> {
-        if cfg!(windows) {
-            run(self.windows, self.work_dir)
-        } else {
-            run(self.unix, self.work_dir)
-        }
-    }
-    pub fn run_with_output(self) -> Result<String> {
-        if cfg!(windows) {
-            run_with_output(self.windows, self.work_dir)
-        } else {
-            run_with_output(self.unix, self.work_dir)
-        }
-    }
-}
-
-pub fn run(cmdline: &str, dir: &str) -> Result<()> {
-    do_run(cmdline, dir, &mut |c| {
-        c.stdout(Stdio::inherit());
-    })
-    .map(|_| ())
-}
-
-pub fn run_with_output(cmdline: &str, dir: &str) -> Result<String> {
-    let output = do_run(cmdline, dir, &mut |_| {})?;
-    let stdout = String::from_utf8(output.stdout)?;
-    let stdout = stdout.trim().to_string();
-    Ok(stdout)
-}
-
-fn do_run(cmdline: &str, dir: &str, f: &mut dyn FnMut(&mut Command)) -> Result<Output> {
-    eprintln!("\nwill run: {}", cmdline);
-    let proj_dir = project_root().join(dir);
-    let mut args = cmdline.split_whitespace();
-    let exec = args.next().unwrap();
-    let mut cmd = Command::new(exec);
-    f(cmd.args(args).current_dir(proj_dir).stderr(Stdio::inherit()));
-    let output = cmd.output().with_context(|| format!("running `{}`", cmdline))?;
-    if !output.status.success() {
-        anyhow::bail!("`{}` exited with {}", cmdline, output.status);
-    }
-    Ok(output)
-}
diff --git a/xtask/src/install.rs b/xtask/src/install.rs
index 99e1eddb162..540a66130bd 100644
--- a/xtask/src/install.rs
+++ b/xtask/src/install.rs
@@ -2,9 +2,9 @@
 
 use std::{env, path::PathBuf, str};
 
-use anyhow::{Context, Result};
+use anyhow::{bail, format_err, Context, Result};
 
-use crate::cmd::{run, run_with_output, Cmd};
+use crate::not_bash::{ls, pushd, rm, run};
 
 // Latest stable, feel free to send a PR if this lags behind.
 const REQUIRED_RUST_VERSION: u32 = 41;
@@ -55,7 +55,7 @@ fn fix_path_for_mac() -> Result<()> {
         const ROOT_DIR: &str = "";
         let home_dir = match env::var("HOME") {
             Ok(home) => home,
-            Err(e) => anyhow::bail!("Failed getting HOME from environment with error: {}.", e),
+            Err(e) => bail!("Failed getting HOME from environment with error: {}.", e),
         };
 
         [ROOT_DIR, &home_dir]
@@ -69,7 +69,7 @@ fn fix_path_for_mac() -> Result<()> {
     if !vscode_path.is_empty() {
         let vars = match env::var_os("PATH") {
             Some(path) => path,
-            None => anyhow::bail!("Could not get PATH variable from env."),
+            None => bail!("Could not get PATH variable from env."),
         };
 
         let mut paths = env::split_paths(&vars).collect::<Vec<_>>();
@@ -82,84 +82,61 @@ fn fix_path_for_mac() -> Result<()> {
 }
 
 fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> {
-    let npm_version = Cmd {
-        unix: r"npm --version",
-        windows: r"cmd.exe /c npm --version",
-        work_dir: "./editors/code",
-    }
-    .run();
+    let _dir = pushd("./editors/code");
 
-    if npm_version.is_err() {
-        eprintln!("\nERROR: `npm --version` failed, `npm` is required to build the VS Code plugin")
-    }
-
-    Cmd { unix: r"npm install", windows: r"cmd.exe /c npm install", work_dir: "./editors/code" }
-        .run()?;
-    Cmd {
-        unix: r"npm run package --scripts-prepend-node-path",
-        windows: r"cmd.exe /c npm run package",
-        work_dir: "./editors/code",
-    }
-    .run()?;
-
-    let code_binary = ["code", "code-insiders", "codium", "code-oss"].iter().find(|bin| {
-        Cmd {
-            unix: &format!("{} --version", bin),
-            windows: &format!("cmd.exe /c {}.cmd --version", bin),
-            work_dir: "./editors/code",
-        }
-        .run()
-        .is_ok()
-    });
-
-    let code_binary = match code_binary {
-        Some(it) => it,
-        None => anyhow::bail!("Can't execute `code --version`. Perhaps it is not in $PATH?"),
+    let find_code = |f: fn(&str) -> bool| -> Result<&'static str> {
+        ["code", "code-insiders", "codium", "code-oss"]
+            .iter()
+            .copied()
+            .find(|bin| f(bin))
+            .ok_or_else(|| {
+                format_err!("Can't execute `code --version`. Perhaps it is not in $PATH?")
+            })
     };
 
-    Cmd {
-        unix: &format!(r"{} --install-extension ./rust-analyzer-0.1.0.vsix --force", code_binary),
-        windows: &format!(
-            r"cmd.exe /c {}.cmd --install-extension ./rust-analyzer-0.1.0.vsix --force",
-            code_binary
-        ),
-        work_dir: "./editors/code",
-    }
-    .run()?;
+    let installed_extensions;
+    if cfg!(unix) {
+        run!("npm --version").context("`npm` is required to build the VS Code plugin")?;
+        run!("npm install")?;
 
-    let installed_extensions = Cmd {
-        unix: &format!(r"{} --list-extensions", code_binary),
-        windows: &format!(r"cmd.exe /c {}.cmd --list-extensions", code_binary),
-        work_dir: ".",
+        let vsix_pkg = {
+            rm("*.vsix")?;
+            run!("npm run package --scripts-prepend-node-path")?;
+            ls("*.vsix")?.pop().unwrap()
+        };
+
+        let code = find_code(|bin| run!("{} --version", bin).is_ok())?;
+        run!("{} --install-extension {} --force", code, vsix_pkg.display())?;
+        installed_extensions = run!("{} --list-extensions", code; echo = false)?;
+    } else {
+        run!("cmd.exe /c npm --version")
+            .context("`npm` is required to build the VS Code plugin")?;
+        run!("cmd.exe /c npm install")?;
+
+        let vsix_pkg = {
+            rm("*.vsix")?;
+            run!("cmd.exe /c npm run package")?;
+            ls("*.vsix")?.pop().unwrap()
+        };
+
+        let code = find_code(|bin| run!("cmd.exe /c {}.cmd --version", bin).is_ok())?;
+        run!(r"cmd.exe /c {}.cmd --install-extension {} --force", code, vsix_pkg.display())?;
+        installed_extensions = run!("cmd.exe /c {}.cmd --list-extensions", code; echo = false)?;
     }
-    .run_with_output()?;
 
     if !installed_extensions.contains("rust-analyzer") {
-        anyhow::bail!(
+        bail!(
             "Could not install the Visual Studio Code extension. \
              Please make sure you have at least NodeJS 10.x together with the latest version of VS Code installed and try again."
         );
     }
 
-    if installed_extensions.contains("ra-lsp") {
-        Cmd {
-            unix: &format!(r"{} --uninstall-extension matklad.ra-lsp", code_binary),
-            windows: &format!(
-                r"cmd.exe /c {}.cmd --uninstall-extension matklad.ra-lsp",
-                code_binary
-            ),
-            work_dir: "./editors/code",
-        }
-        .run()?;
-    }
-
     Ok(())
 }
 
 fn install_server(opts: ServerOpt) -> Result<()> {
     let mut old_rust = false;
-    if let Ok(stdout) = run_with_output("cargo --version", ".") {
-        println!("{}", stdout);
+    if let Ok(stdout) = run!("cargo --version") {
         if !check_version(&stdout, REQUIRED_RUST_VERSION) {
             old_rust = true;
         }
@@ -172,20 +149,17 @@ fn install_server(opts: ServerOpt) -> Result<()> {
         )
     }
 
-    let res = if opts.jemalloc {
-        run("cargo install --path crates/ra_lsp_server --locked --force --features jemalloc", ".")
-    } else {
-        run("cargo install --path crates/ra_lsp_server --locked --force", ".")
-    };
+    let jemalloc = if opts.jemalloc { "--features jemalloc" } else { "" };
+    let res = run!("cargo install --path crates/ra_lsp_server --locked --force {}", jemalloc);
 
     if res.is_err() && old_rust {
         eprintln!(
             "\nWARNING: at least rust 1.{}.0 is required to compile rust-analyzer\n",
             REQUIRED_RUST_VERSION,
-        )
+        );
     }
 
-    res
+    res.map(drop)
 }
 
 fn check_version(version_output: &str, min_minor_version: u32) -> bool {
diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs
index 1bb1882b0f0..d2ef2e95b74 100644
--- a/xtask/src/lib.rs
+++ b/xtask/src/lib.rs
@@ -1,6 +1,6 @@
 //! FIXME: write short doc here
 
-mod cmd;
+pub mod not_bash;
 pub mod install;
 pub mod pre_commit;
 
@@ -16,8 +16,8 @@ use std::{
 };
 
 use crate::{
-    cmd::{run, run_with_output},
     codegen::Mode,
+    not_bash::{pushd, run},
 };
 
 pub use anyhow::Result;
@@ -38,9 +38,9 @@ pub fn run_rustfmt(mode: Mode) -> Result<()> {
     ensure_rustfmt()?;
 
     if mode == Mode::Verify {
-        run(&format!("rustup run {} -- cargo fmt -- --check", TOOLCHAIN), ".")?;
+        run!("rustup run {} -- cargo fmt -- --check", TOOLCHAIN)?;
     } else {
-        run(&format!("rustup run {} -- cargo fmt", TOOLCHAIN), ".")?;
+        run!("rustup run {} -- cargo fmt", TOOLCHAIN)?;
     }
     Ok(())
 }
@@ -70,8 +70,9 @@ fn ensure_rustfmt() -> Result<()> {
         Ok(status) if status.success() => return Ok(()),
         _ => (),
     };
-    run(&format!("rustup toolchain install {}", TOOLCHAIN), ".")?;
-    run(&format!("rustup component add rustfmt --toolchain {}", TOOLCHAIN), ".")
+    run!("rustup toolchain install {}", TOOLCHAIN)?;
+    run!("rustup component add rustfmt --toolchain {}", TOOLCHAIN)?;
+    Ok(())
 }
 
 pub fn run_clippy() -> Result<()> {
@@ -92,34 +93,31 @@ pub fn run_clippy() -> Result<()> {
         "clippy::nonminimal_bool",
         "clippy::redundant_pattern_matching",
     ];
-    run(
-        &format!(
-            "rustup run {} -- cargo clippy --all-features --all-targets -- -A {}",
-            TOOLCHAIN,
-            allowed_lints.join(" -A ")
-        ),
-        ".",
+    run!(
+        "rustup run {} -- cargo clippy --all-features --all-targets -- -A {}",
+        TOOLCHAIN,
+        allowed_lints.join(" -A ")
     )?;
     Ok(())
 }
 
 fn install_clippy() -> Result<()> {
-    run(&format!("rustup toolchain install {}", TOOLCHAIN), ".")?;
-    run(&format!("rustup component add clippy --toolchain {}", TOOLCHAIN), ".")
+    run!("rustup toolchain install {}", TOOLCHAIN)?;
+    run!("rustup component add clippy --toolchain {}", TOOLCHAIN)?;
+    Ok(())
 }
 
 pub fn run_fuzzer() -> Result<()> {
-    match Command::new("cargo")
-        .args(&["fuzz", "--help"])
-        .stderr(Stdio::null())
-        .stdout(Stdio::null())
-        .status()
-    {
-        Ok(status) if status.success() => (),
-        _ => run("cargo install cargo-fuzz", ".")?,
+    let _d = pushd("./crates/ra_syntax");
+    match run!("cargo fuzz --help") {
+        Ok(_) => (),
+        _ => {
+            run!("cargo install cargo-fuzz")?;
+        }
     };
 
-    run("rustup run nightly -- cargo fuzz run parser", "./crates/ra_syntax")
+    run!("rustup run nightly -- cargo fuzz run parser")?;
+    Ok(())
 }
 
 /// Cleans the `./target` dir after the build such that only
@@ -161,15 +159,15 @@ fn rm_rf(path: &Path) -> Result<()> {
 }
 
 pub fn run_release() -> Result<()> {
-    run("git switch release", ".")?;
-    run("git fetch upstream", ".")?;
-    run("git reset --hard upstream/master", ".")?;
-    run("git push", ".")?;
+    run!("git switch release")?;
+    run!("git fetch upstream")?;
+    run!("git reset --hard upstream/master")?;
+    run!("git push")?;
 
     let changelog_dir = project_root().join("../rust-analyzer.github.io/thisweek/_posts");
 
-    let today = run_with_output("date --iso", ".")?;
-    let commit = run_with_output("git rev-parse HEAD", ".")?;
+    let today = run!("date --iso")?;
+    let commit = run!("git rev-parse HEAD")?;
     let changelog_n = fs::read_dir(changelog_dir.as_path())?.count();
 
     let contents = format!(
diff --git a/xtask/src/not_bash.rs b/xtask/src/not_bash.rs
new file mode 100644
index 00000000000..56d6c6c2d83
--- /dev/null
+++ b/xtask/src/not_bash.rs
@@ -0,0 +1,123 @@
+//! A bad shell -- small cross platform module for writing glue code
+use std::{
+    cell::RefCell,
+    env,
+    ffi::OsStr,
+    fs,
+    path::PathBuf,
+    process::{Command, Stdio},
+};
+
+use anyhow::{bail, Context, Result};
+
+macro_rules! _run {
+    ($($expr:expr),*) => {
+        run!($($expr),*; echo = true)
+    };
+    ($($expr:expr),* ; echo = $echo:expr) => {
+        $crate::not_bash::run_process(format!($($expr),*), $echo)
+    };
+}
+pub(crate) use _run as run;
+
+pub struct Pushd {
+    _p: (),
+}
+
+pub fn pushd(path: impl Into<PathBuf>) -> Pushd {
+    Env::with(|env| env.pushd(path.into()));
+    Pushd { _p: () }
+}
+
+impl Drop for Pushd {
+    fn drop(&mut self) {
+        Env::with(|env| env.popd())
+    }
+}
+
+pub fn rm(glob: &str) -> Result<()> {
+    let cwd = Env::with(|env| env.cwd());
+    ls(glob)?.into_iter().try_for_each(|it| fs::remove_file(cwd.join(it)))?;
+    Ok(())
+}
+
+pub fn ls(glob: &str) -> Result<Vec<PathBuf>> {
+    let cwd = Env::with(|env| env.cwd());
+    let mut res = Vec::new();
+    for entry in fs::read_dir(&cwd)? {
+        let entry = entry?;
+        if matches(&entry.file_name(), glob) {
+            let path = entry.path();
+            let path = path.strip_prefix(&cwd).unwrap();
+            res.push(path.to_path_buf())
+        }
+    }
+    return Ok(res);
+
+    fn matches(file_name: &OsStr, glob: &str) -> bool {
+        assert!(glob.starts_with('*'));
+        file_name.to_string_lossy().ends_with(&glob[1..])
+    }
+}
+
+#[doc(hidden)]
+pub fn run_process(cmd: String, echo: bool) -> Result<String> {
+    run_process_inner(&cmd, echo).with_context(|| format!("process `{}` failed", cmd))
+}
+
+fn run_process_inner(cmd: &str, echo: bool) -> Result<String> {
+    let cwd = Env::with(|env| env.cwd());
+    let mut args = shelx(cmd);
+    let binary = args.remove(0);
+
+    if echo {
+        println!("> {}", cmd)
+    }
+
+    let output = Command::new(binary)
+        .args(args)
+        .current_dir(cwd)
+        .stdin(Stdio::null())
+        .stderr(Stdio::inherit())
+        .output()?;
+    let stdout = String::from_utf8(output.stdout)?;
+
+    if echo {
+        print!("{}", stdout)
+    }
+
+    if !output.status.success() {
+        bail!("{}", output.status)
+    }
+
+    Ok(stdout)
+}
+
+// FIXME: some real shell lexing here
+fn shelx(cmd: &str) -> Vec<String> {
+    cmd.split_whitespace().map(|it| it.to_string()).collect()
+}
+
+#[derive(Default)]
+struct Env {
+    pushd_stack: Vec<PathBuf>,
+}
+
+impl Env {
+    fn with<F: FnOnce(&mut Env) -> T, T>(f: F) -> T {
+        thread_local! {
+            static ENV: RefCell<Env> = Default::default();
+        }
+        ENV.with(|it| f(&mut *it.borrow_mut()))
+    }
+
+    fn pushd(&mut self, dir: PathBuf) {
+        self.pushd_stack.push(dir)
+    }
+    fn popd(&mut self) {
+        self.pushd_stack.pop().unwrap();
+    }
+    fn cwd(&self) -> PathBuf {
+        self.pushd_stack.last().cloned().unwrap_or_else(|| env::current_dir().unwrap())
+    }
+}
diff --git a/xtask/src/pre_commit.rs b/xtask/src/pre_commit.rs
index 1533f64dc0b..056f34acfb8 100644
--- a/xtask/src/pre_commit.rs
+++ b/xtask/src/pre_commit.rs
@@ -4,18 +4,18 @@ use std::{fs, path::PathBuf};
 
 use anyhow::{bail, Result};
 
-use crate::{cmd::run_with_output, project_root, run, run_rustfmt, Mode};
+use crate::{not_bash::run, project_root, run_rustfmt, Mode};
 
 // FIXME: if there are changed `.ts` files, also reformat TypeScript (by
 // shelling out to `npm fmt`).
 pub fn run_hook() -> Result<()> {
     run_rustfmt(Mode::Overwrite)?;
 
-    let diff = run_with_output("git diff --diff-filter=MAR --name-only --cached", ".")?;
+    let diff = run!("git diff --diff-filter=MAR --name-only --cached")?;
 
     let root = project_root();
     for line in diff.lines() {
-        run(&format!("git update-index --add {}", root.join(line).to_string_lossy()), ".")?;
+        run!("git update-index --add {}", root.join(line).display())?;
     }
 
     Ok(())