Repository: bytecodealliance/wasmtime
Pull request #6849 (2023-08-15 15:58:09)

CI: upgrade to qemu 8.0.4.
This should resolve a bug in qemu triggered by changes in #6820 (see
[here](https://github.com/bytecodealliance/wasmtime/pull/6820#issuecomment-1678908361)).

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9d64ba405..702c4f25c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -373,7 +373,7 @@ jobs:
     name: ${{ matrix.name }}
     runs-on: ${{ matrix.os }}
     env:
-      QEMU_BUILD_VERSION: 7.2.0
+      QEMU_BUILD_VERSION: 8.0.4
     strategy:
       fail-fast: true
       matrix: ${{ fromJson(needs.determine.outputs.test-matrix) }}
diff --git a/Cargo.lock b/Cargo.lock
index a21132425..f5bcb273c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2648,6 +2648,7 @@ dependencies = [
  "http-body",
  "http-body-util",
  "hyper",
+ "is-terminal",
  "lazy_static",
  "tempfile",
  "test-log",
@@ -3733,6 +3734,7 @@ dependencies = [
  "fs-set-times",
  "futures",
  "io-extras",
+ "is-terminal",
  "libc",
  "once_cell",
  "rustix 0.38.4",
diff --git a/Cargo.toml b/Cargo.toml
index 27f08de6d..d45779629 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -195,7 +195,7 @@ system-interface = { version = "0.26.0", features = ["cap_std_impls"] }
 io-lifetimes = { version = "2.0.2", default-features = false }
 io-extras = "0.18.0"
 rustix = "0.38.4"
-
+is-terminal = "0.4.0"
 # wit-bindgen:
 wit-bindgen = { version = "0.9.0", default-features = false }
 
diff --git a/ci/qemu-cpuinfo.patch b/ci/qemu-cpuinfo.patch
index 44a4e23b2..261bcb593 100644
--- a/ci/qemu-cpuinfo.patch
+++ b/ci/qemu-cpuinfo.patch
@@ -1,4 +1,4 @@
-From 529c68a118055c95336f91e4d166366a23a943f4 Mon Sep 17 00:00:00 2001
+From 5d05b046efb079360d0175164ee04947f20f9e66 Mon Sep 17 00:00:00 2001
 From: Afonso Bordado <afonsobordado@az8.co>
 Date: Tue, 21 Mar 2023 18:45:20 +0000
 Subject: [PATCH] linux-user: Emulate /proc/cpuinfo output for riscv
@@ -10,17 +10,16 @@ Currently when querying this file the host's file is shown instead
 which is slightly confusing. Emulate a basic /proc/cpuinfo file
 with mmu info and an ISA string.
 ---
- linux-user/syscall.c              | 33 +++++++++++++++++++++++++++++--
- tests/tcg/riscv64/Makefile.target |  1 +
- tests/tcg/riscv64/cpuinfo.c       | 30 ++++++++++++++++++++++++++++
- 3 files changed, 62 insertions(+), 2 deletions(-)
+ linux-user/syscall.c        | 34 ++++++++++++++++++++++++++++++++--
+ tests/tcg/riscv64/cpuinfo.c | 30 ++++++++++++++++++++++++++++++
+ 2 files changed, 62 insertions(+), 2 deletions(-)
  create mode 100644 tests/tcg/riscv64/cpuinfo.c
 
 diff --git a/linux-user/syscall.c b/linux-user/syscall.c
-index 24b25759be..2b8a588be2 100644
+index 333e6b7026..e6d85e0e8a 100644
 --- a/linux-user/syscall.c
 +++ b/linux-user/syscall.c
-@@ -8206,7 +8206,8 @@ void target_exception_dump(CPUArchState *env, const char *fmt, int code)
+@@ -8231,7 +8231,8 @@ void target_exception_dump(CPUArchState *env, const char *fmt, int code)
  }
  
  #if HOST_BIG_ENDIAN != TARGET_BIG_ENDIAN || \
@@ -30,7 +29,7 @@ index 24b25759be..2b8a588be2 100644
  static int is_proc(const char *filename, const char *entry)
  {
      return strcmp(filename, entry) == 0;
-@@ -8278,6 +8279,34 @@ static int open_cpuinfo(CPUArchState *cpu_env, int fd)
+@@ -8309,6 +8310,35 @@ static int open_cpuinfo(CPUArchState *cpu_env, int fd)
  }
  #endif
  
@@ -40,10 +39,11 @@ index 24b25759be..2b8a588be2 100644
 +    int i;
 +    int num_cpus = sysconf(_SC_NPROCESSORS_ONLN);
 +    RISCVCPU *cpu = env_archcpu(cpu_env);
++    const RISCVCPUConfig *cfg = riscv_cpu_cfg((CPURISCVState *) cpu_env);
 +    char *isa_string = riscv_isa_string(cpu);
 +    const char *mmu;
 +
-+    if (cpu->cfg.mmu) {
++    if (cfg->mmu) {
 +        mmu = (cpu_env->xl == MXL_RV32) ? "sv32"  : "sv48";
 +    } else {
 +        mmu = "none";
@@ -65,7 +65,7 @@ index 24b25759be..2b8a588be2 100644
  #if defined(TARGET_M68K)
  static int open_hardware(CPUArchState *cpu_env, int fd)
  {
-@@ -8302,7 +8331,7 @@ static int do_openat(CPUArchState *cpu_env, int dirfd, const char *pathname, int
+@@ -8333,7 +8363,7 @@ static int do_openat(CPUArchState *cpu_env, int dirfd, const char *pathname, int
  #if HOST_BIG_ENDIAN != TARGET_BIG_ENDIAN
          { "/proc/net/route", open_net_route, is_proc },
  #endif
@@ -74,16 +74,6 @@ index 24b25759be..2b8a588be2 100644
          { "/proc/cpuinfo", open_cpuinfo, is_proc },
  #endif
  #if defined(TARGET_M68K)
-diff --git a/tests/tcg/riscv64/Makefile.target b/tests/tcg/riscv64/Makefile.target
-index b5b89dfb0e..f455e19a11 100644
---- a/tests/tcg/riscv64/Makefile.target
-+++ b/tests/tcg/riscv64/Makefile.target
-@@ -4,3 +4,4 @@
- VPATH += $(SRC_PATH)/tests/tcg/riscv64
- TESTS += test-div
- TESTS += noexec
-+TESTS += cpuinfo
-\ No newline at end of file
 diff --git a/tests/tcg/riscv64/cpuinfo.c b/tests/tcg/riscv64/cpuinfo.c
 new file mode 100644
 index 0000000000..296abd0a8c
diff --git a/crates/wasi-common/cap-std-sync/Cargo.toml b/crates/wasi-common/cap-std-sync/Cargo.toml
index 64fb8efb3..f0fe058ea 100644
--- a/crates/wasi-common/cap-std-sync/Cargo.toml
+++ b/crates/wasi-common/cap-std-sync/Cargo.toml
@@ -23,7 +23,7 @@ fs-set-times = { workspace = true }
 system-interface = { workspace = true, features = ["cap_std_impls"] }
 tracing = { workspace = true }
 io-lifetimes = { workspace = true }
-is-terminal = "0.4.0"
+is-terminal = { workspace = true }
 
 [target.'cfg(unix)'.dependencies]
 rustix = { workspace = true, features = ["fs", "event"] }
diff --git a/crates/wasi-preview1-component-adapter/src/descriptors.rs b/crates/wasi-preview1-component-adapter/src/descriptors.rs
index e8412eb01..81d8f2810 100644
--- a/crates/wasi-preview1-component-adapter/src/descriptors.rs
+++ b/crates/wasi-preview1-component-adapter/src/descriptors.rs
@@ -1,4 +1,7 @@
-use crate::bindings::wasi::cli_base::{stderr, stdin, stdout};
+use crate::bindings::wasi::cli::{
+    stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, terminal_stdin,
+    terminal_stdout,
+};
 use crate::bindings::wasi::filesystem::types as filesystem;
 use crate::bindings::wasi::io::streams::{self, InputStream, OutputStream};
 use crate::bindings::wasi::sockets::tcp;
@@ -32,7 +35,7 @@ impl Drop for Descriptor {
                 match &stream.type_ {
                     StreamType::File(file) => filesystem::drop_descriptor(file.fd),
                     StreamType::Socket(_) => unreachable!(),
-                    StreamType::Stdio => {}
+                    StreamType::Stdio(_) => {}
                 }
             }
             Descriptor::Closed(_) => {}
@@ -109,7 +112,7 @@ impl Streams {
 #[allow(dead_code)] // until Socket is implemented
 pub enum StreamType {
     /// Stream is used for implementing stdio.
-    Stdio,
+    Stdio(IsATTY),
 
     /// Streaming data with a file.
     File(File),
@@ -118,6 +121,20 @@ pub enum StreamType {
     Socket(tcp::TcpSocket),
 }
 
+pub enum IsATTY {
+    Yes,
+    No,
+}
+
+impl IsATTY {
+    pub fn filetype(&self) -> wasi::Filetype {
+        match self {
+            IsATTY::Yes => wasi::FILETYPE_CHARACTER_DEVICE,
+            IsATTY::No => wasi::FILETYPE_UNKNOWN,
+        }
+    }
+}
+
 #[repr(C)]
 pub struct Descriptors {
     /// Storage of mapping from preview1 file descriptors to preview2 file
@@ -143,26 +160,47 @@ impl Descriptors {
         };
 
         let stdin = stdin::get_stdin();
+        let stdin_isatty = match terminal_stdin::get_terminal_stdin() {
+            Some(t) => {
+                terminal_input::drop_terminal_input(t);
+                IsATTY::Yes
+            }
+            None => IsATTY::No,
+        };
         let stdout = stdout::get_stdout();
+        let stdout_isatty = match terminal_stdout::get_terminal_stdout() {
+            Some(t) => {
+                terminal_output::drop_terminal_output(t);
+                IsATTY::Yes
+            }
+            None => IsATTY::No,
+        };
         let stderr = stderr::get_stderr();
         unsafe { set_stderr_stream(stderr) };
+        let stderr_isatty = match terminal_stderr::get_terminal_stderr() {
+            Some(t) => {
+                terminal_output::drop_terminal_output(t);
+                IsATTY::Yes
+            }
+            None => IsATTY::No,
+        };
 
         d.push(Descriptor::Streams(Streams {
             input: Cell::new(Some(stdin)),
             output: Cell::new(None),
-            type_: StreamType::Stdio,
+            type_: StreamType::Stdio(stdin_isatty),
         }))
         .trapping_unwrap();
         d.push(Descriptor::Streams(Streams {
             input: Cell::new(None),
             output: Cell::new(Some(stdout)),
-            type_: StreamType::Stdio,
+            type_: StreamType::Stdio(stdout_isatty),
         }))
         .trapping_unwrap();
         d.push(Descriptor::Streams(Streams {
             input: Cell::new(None),
             output: Cell::new(Some(stderr)),
-            type_: StreamType::Stdio,
+            type_: StreamType::Stdio(stderr_isatty),
         }))
         .trapping_unwrap();
 
diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs
index 67179891c..595ffe615 100644
--- a/crates/wasi-preview1-component-adapter/src/lib.rs
+++ b/crates/wasi-preview1-component-adapter/src/lib.rs
@@ -1,6 +1,6 @@
 #![allow(unused_variables)] // TODO: remove this when more things are implemented
 
-use crate::bindings::wasi::cli_base::exit;
+use crate::bindings::wasi::cli::exit;
 use crate::bindings::wasi::clocks::{monotonic_clock, wall_clock};
 use crate::bindings::wasi::filesystem::types as filesystem;
 use crate::bindings::wasi::io::streams;
@@ -25,13 +25,13 @@ compile_error!("only one of the `command` and `reactor` features may be selected
 mod macros;
 
 mod descriptors;
-use crate::descriptors::{Descriptor, Descriptors, StreamType, Streams};
+use crate::descriptors::{Descriptor, Descriptors, IsATTY, StreamType, Streams};
 
 pub mod bindings {
     #[cfg(feature = "command")]
     wit_bindgen::generate!({
         path: "../wasi/wit",
-        world: "wasi:preview/command",
+        world: "wasi:cli/command",
         std_feature,
         raw_strings,
         // Automatically generated bindings for these functions will allocate
@@ -46,7 +46,7 @@ pub mod bindings {
     #[cfg(feature = "reactor")]
     wit_bindgen::generate!({
         path: "../wasi/wit",
-        world: "wasi:preview/reactor",
+        world: "wasmtime:wasi/preview1-adapter-reactor",
         std_feature,
         raw_strings,
         // Automatically generated bindings for these functions will allocate
@@ -59,7 +59,7 @@ pub mod bindings {
     });
 }
 
-#[no_mangle]
+#[export_name = "wasi:cli/run#run"]
 #[cfg(feature = "command")]
 pub unsafe extern "C" fn run() -> u32 {
     #[link(wasm_import_module = "__main_module__")]
@@ -564,14 +564,8 @@ pub unsafe extern "C" fn fd_fdstat_get(fd: Fd, stat: *mut Fdstat) -> Errno {
         Descriptor::Streams(Streams {
             input,
             output,
-            type_: StreamType::Socket(_),
-        })
-        | Descriptor::Streams(Streams {
-            input,
-            output,
-            type_: StreamType::Stdio,
+            type_: StreamType::Stdio(isatty),
         }) => {
-            let fs_filetype = FILETYPE_CHARACTER_DEVICE;
             let fs_flags = 0;
             let mut fs_rights_base = 0;
             if input.get().is_some() {
@@ -582,7 +576,7 @@ pub unsafe extern "C" fn fd_fdstat_get(fd: Fd, stat: *mut Fdstat) -> Errno {
             }
             let fs_rights_inheriting = fs_rights_base;
             stat.write(Fdstat {
-                fs_filetype,
+                fs_filetype: isatty.filetype(),
                 fs_flags,
                 fs_rights_base,
                 fs_rights_inheriting,
@@ -590,6 +584,11 @@ pub unsafe extern "C" fn fd_fdstat_get(fd: Fd, stat: *mut Fdstat) -> Errno {
             Ok(())
         }
         Descriptor::Closed(_) => Err(ERRNO_BADF),
+        Descriptor::Streams(Streams {
+            input,
+            output,
+            type_: StreamType::Socket(_),
+        }) => unreachable!(),
     })
 }
 
@@ -657,13 +656,13 @@ pub unsafe extern "C" fn fd_filestat_get(fd: Fd, buf: *mut Filestat) -> Errno {
             }
             // Stdio is all zero fields, except for filetype character device
             Descriptor::Streams(Streams {
-                type_: StreamType::Stdio,
+                type_: StreamType::Stdio(isatty),
                 ..
             }) => {
                 *buf = Filestat {
                     dev: 0,
                     ino: 0,
-                    filetype: FILETYPE_CHARACTER_DEVICE,
+                    filetype: isatty.filetype(),
                     nlink: 0,
                     size: 0,
                     atim: 0,
@@ -1848,7 +1847,7 @@ pub unsafe extern "C" fn poll_oneoff(
                                                }
                                                */
                             }
-                            StreamType::Stdio => {
+                            StreamType::Stdio(_) => {
                                 error = ERRNO_SUCCESS;
                                 nbytes = 1;
                                 flags = 0;
@@ -1865,7 +1864,7 @@ pub unsafe extern "C" fn poll_oneoff(
                         .trapping_unwrap();
                     match desc {
                         Descriptor::Streams(streams) => match streams.type_ {
-                            StreamType::File(_) | StreamType::Stdio => {
+                            StreamType::File(_) | StreamType::Stdio(_) => {
                                 error = ERRNO_SUCCESS;
                                 nbytes = 1;
                                 flags = 0;
@@ -2430,7 +2429,7 @@ impl State {
 
     fn get_environment(&self) -> &[StrTuple] {
         if self.env_vars.get().is_none() {
-            #[link(wasm_import_module = "wasi:cli-base/environment")]
+            #[link(wasm_import_module = "wasi:cli/environment")]
             extern "C" {
                 #[link_name = "get-environment"]
                 fn get_environment_import(rval: *mut StrTupleList);
@@ -2454,7 +2453,7 @@ impl State {
 
     fn get_args(&self) -> &[WasmStr] {
         if self.args.get().is_none() {
-            #[link(wasm_import_module = "wasi:cli-base/environment")]
+            #[link(wasm_import_module = "wasi:cli/environment")]
             extern "C" {
                 #[link_name = "get-arguments"]
                 fn get_args_import(rval: *mut WasmStrList);
diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml
index ab6655a79..462dccbae 100644
--- a/crates/wasi/Cargo.toml
+++ b/crates/wasi/Cargo.toml
@@ -31,6 +31,7 @@ cap-rand = { workspace = true, optional = true }
 cap-fs-ext = { workspace = true, optional = true }
 cap-time-ext = { workspace = true, optional = true }
 fs-set-times = { workspace = true, optional = true }
+is-terminal = { workspace = true, optional = true }
 bitflags = { workspace = true, optional = true }
 async-trait = { workspace = true, optional = true }
 system-interface = { workspace = true, optional = true}
@@ -64,6 +65,7 @@ preview2 = [
     'dep:cap-fs-ext',
     'dep:cap-time-ext',
     'dep:fs-set-times',
+    'dep:is-terminal',
     'dep:bitflags',
     'dep:async-trait',
     'dep:system-interface',
diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs
index e213f3e4b..d44b4026a 100644
--- a/crates/wasi/src/preview2/command.rs
+++ b/crates/wasi/src/preview2/command.rs
@@ -1,7 +1,7 @@
 use crate::preview2::WasiView;
 
 wasmtime::component::bindgen!({
-    world: "wasi:preview/command",
+    world: "wasi:cli/command",
     tracing: true,
     async: true,
     trappable_error_type: {
@@ -17,11 +17,16 @@ wasmtime::component::bindgen!({
        "wasi:clocks/timezone": crate::preview2::bindings::clocks::timezone,
        "wasi:clocks/wall_clock": crate::preview2::bindings::clocks::wall_clock,
        "wasi:random/random": crate::preview2::bindings::random::random,
-       "wasi:cli_base/environment": crate::preview2::bindings::cli_base::environment,
-       "wasi:cli_base/exit": crate::preview2::bindings::cli_base::exit,
-       "wasi:cli_base/stdin": crate::preview2::bindings::cli_base::stdin,
-       "wasi:cli_base/stdout": crate::preview2::bindings::cli_base::stdout,
-       "wasi:cli_base/stderr": crate::preview2::bindings::cli_base::stderr,
+       "wasi:cli/environment": crate::preview2::bindings::cli::environment,
+       "wasi:cli/exit": crate::preview2::bindings::cli::exit,
+       "wasi:cli/stdin": crate::preview2::bindings::cli::stdin,
+       "wasi:cli/stdout": crate::preview2::bindings::cli::stdout,
+       "wasi:cli/stderr": crate::preview2::bindings::cli::stderr,
+       "wasi:cli/terminal-input": crate::preview2::bindings::cli::terminal_input,
+       "wasi:cli/terminal-output": crate::preview2::bindings::cli::terminal_output,
+       "wasi:cli/terminal-stdin": crate::preview2::bindings::cli::terminal_stdin,
+       "wasi:cli/terminal-stdout": crate::preview2::bindings::cli::terminal_stdout,
+       "wasi:cli/terminal-stderr": crate::preview2::bindings::cli::terminal_stderr,
     },
 });
 
@@ -34,11 +39,16 @@ pub fn add_to_linker<T: WasiView>(l: &mut wasmtime::component::Linker<T>) -> any
     crate::preview2::bindings::poll::poll::add_to_linker(l, |t| t)?;
     crate::preview2::bindings::io::streams::add_to_linker(l, |t| t)?;
     crate::preview2::bindings::random::random::add_to_linker(l, |t| t)?;
-    crate::preview2::bindings::cli_base::exit::add_to_linker(l, |t| t)?;
-    crate::preview2::bindings::cli_base::environment::add_to_linker(l, |t| t)?;
-    crate::preview2::bindings::cli_base::stdin::add_to_linker(l, |t| t)?;
-    crate::preview2::bindings::cli_base::stdout::add_to_linker(l, |t| t)?;
-    crate::preview2::bindings::cli_base::stderr::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::exit::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::environment::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::stdin::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::stdout::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::stderr::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::terminal_input::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::terminal_output::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::terminal_stdin::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::terminal_stdout::add_to_linker(l, |t| t)?;
+    crate::preview2::bindings::cli::terminal_stderr::add_to_linker(l, |t| t)?;
     Ok(())
 }
 
@@ -46,7 +56,7 @@ pub mod sync {
     use crate::preview2::WasiView;
 
     wasmtime::component::bindgen!({
-        world: "wasi:preview/command",
+        world: "wasi:cli/command",
         tracing: true,
         async: false,
         trappable_error_type: {
@@ -62,11 +72,16 @@ pub mod sync {
            "wasi:clocks/timezone": crate::preview2::bindings::clocks::timezone,
            "wasi:clocks/wall_clock": crate::preview2::bindings::clocks::wall_clock,
            "wasi:random/random": crate::preview2::bindings::random::random,
-           "wasi:cli_base/environment": crate::preview2::bindings::cli_base::environment,
-           "wasi:cli_base/exit": crate::preview2::bindings::cli_base::exit,
-           "wasi:cli_base/stdin": crate::preview2::bindings::cli_base::stdin,
-           "wasi:cli_base/stdout": crate::preview2::bindings::cli_base::stdout,
-           "wasi:cli_base/stderr": crate::preview2::bindings::cli_base::stderr,
+           "wasi:cli/environment": crate::preview2::bindings::cli::environment,
+           "wasi:cli/exit": crate::preview2::bindings::cli::exit,
+           "wasi:cli/stdin": crate::preview2::bindings::cli::stdin,
+           "wasi:cli/stdout": crate::preview2::bindings::cli::stdout,
+           "wasi:cli/stderr": crate::preview2::bindings::cli::stderr,
+           "wasi:cli/terminal-input": crate::preview2::bindings::cli::terminal_input,
+           "wasi:cli/terminal-output": crate::preview2::bindings::cli::terminal_output,
+           "wasi:cli/terminal-stdin": crate::preview2::bindings::cli::terminal_stdin,
+           "wasi:cli/terminal-stdout": crate::preview2::bindings::cli::terminal_stdout,
+           "wasi:cli/terminal-stderr": crate::preview2::bindings::cli::terminal_stderr,
         },
     });
 
@@ -81,11 +96,16 @@ pub mod sync {
         crate::preview2::bindings::sync_io::poll::poll::add_to_linker(l, |t| t)?;
         crate::preview2::bindings::sync_io::io::streams::add_to_linker(l, |t| t)?;
         crate::preview2::bindings::random::random::add_to_linker(l, |t| t)?;
-        crate::preview2::bindings::cli_base::exit::add_to_linker(l, |t| t)?;
-        crate::preview2::bindings::cli_base::environment::add_to_linker(l, |t| t)?;
-        crate::preview2::bindings::cli_base::stdin::add_to_linker(l, |t| t)?;
-        crate::preview2::bindings::cli_base::stdout::add_to_linker(l, |t| t)?;
-        crate::preview2::bindings::cli_base::stderr::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::exit::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::environment::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::stdin::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::stdout::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::stderr::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::terminal_input::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::terminal_output::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::terminal_stdin::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::terminal_stdout::add_to_linker(l, |t| t)?;
+        crate::preview2::bindings::cli::terminal_stderr::add_to_linker(l, |t| t)?;
         Ok(())
     }
 }
diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs
index 10408fbf2..252307c80 100644
--- a/crates/wasi/src/preview2/ctx.rs
+++ b/crates/wasi/src/preview2/ctx.rs
@@ -3,16 +3,17 @@ use crate::preview2::{
     clocks::{self, HostMonotonicClock, HostWallClock},
     filesystem::{Dir, TableFsExt},
     pipe, random, stdio,
+    stdio::{StdioInput, StdioOutput},
     stream::{HostInputStream, HostOutputStream, TableStreamExt},
-    DirPerms, FilePerms, Table,
+    DirPerms, FilePerms, IsATTY, Table,
 };
 use cap_rand::{Rng, RngCore, SeedableRng};
 use std::mem;
 
 pub struct WasiCtxBuilder {
-    stdin: Box<dyn HostInputStream>,
-    stdout: Box<dyn HostOutputStream>,
-    stderr: Box<dyn HostOutputStream>,
+    stdin: (Box<dyn HostInputStream>, IsATTY),
+    stdout: (Box<dyn HostOutputStream>, IsATTY),
+    stderr: (Box<dyn HostOutputStream>, IsATTY),
     env: Vec<(String, String)>,
     args: Vec<String>,
     preopens: Vec<(Dir, String)>,
@@ -56,9 +57,9 @@ impl WasiCtxBuilder {
         let insecure_random_seed =
             cap_rand::thread_rng(cap_rand::ambient_authority()).gen::<u128>();
         Self {
-            stdin: Box::new(pipe::ClosedInputStream),
-            stdout: Box::new(pipe::SinkOutputStream),
-            stderr: Box::new(pipe::SinkOutputStream),
+            stdin: (Box::new(pipe::ClosedInputStream), IsATTY::No),
+            stdout: (Box::new(pipe::SinkOutputStream), IsATTY::No),
+            stderr: (Box::new(pipe::SinkOutputStream), IsATTY::No),
             env: Vec::new(),
             args: Vec::new(),
             preopens: Vec::new(),
@@ -71,31 +72,52 @@ impl WasiCtxBuilder {
         }
     }
 
-    pub fn stdin(&mut self, stdin: impl HostInputStream + 'static) -> &mut Self {
-        self.stdin = Box::new(stdin);
+    pub fn stdin(&mut self, stdin: impl HostInputStream + 'static, isatty: IsATTY) -> &mut Self {
+        self.stdin = (Box::new(stdin), isatty);
         self
     }
 
-    pub fn stdout(&mut self, stdout: impl HostOutputStream + 'static) -> &mut Self {
-        self.stdout = Box::new(stdout);
+    pub fn stdout(&mut self, stdout: impl HostOutputStream + 'static, isatty: IsATTY) -> &mut Self {
+        self.stdout = (Box::new(stdout), isatty);
         self
     }
 
-    pub fn stderr(&mut self, stderr: impl HostOutputStream + 'static) -> &mut Self {
-        self.stderr = Box::new(stderr);
+    pub fn stderr(&mut self, stderr: impl HostOutputStream + 'static, isatty: IsATTY) -> &mut Self {
+        self.stderr = (Box::new(stderr), isatty);
         self
     }
 
     pub fn inherit_stdin(&mut self) -> &mut Self {
-        self.stdin(stdio::stdin())
+        use is_terminal::IsTerminal;
+        let inherited = stdio::stdin();
+        let isatty = if inherited.is_terminal() {
+            IsATTY::Yes
+        } else {
+            IsATTY::No
+        };
+        self.stdin(inherited, isatty)
     }
 
     pub fn inherit_stdout(&mut self) -> &mut Self {
-        self.stdout(stdio::stdout())
+        use is_terminal::IsTerminal;
+        let inherited = stdio::stdout();
+        let isatty = if inherited.is_terminal() {
+            IsATTY::Yes
+        } else {
+            IsATTY::No
+        };
+        self.stdout(inherited, isatty)
     }
 
     pub fn inherit_stderr(&mut self) -> &mut Self {
-        self.stderr(stdio::stderr())
+        use is_terminal::IsTerminal;
+        let inherited = stdio::stderr();
+        let isatty = if inherited.is_terminal() {
+            IsATTY::Yes
+        } else {
+            IsATTY::No
+        };
+        self.stderr(inherited, isatty)
     }
 
     pub fn inherit_stdio(&mut self) -> &mut Self {
@@ -208,9 +230,9 @@ impl WasiCtxBuilder {
         } = mem::replace(self, Self::new());
         self.built = true;
 
-        let stdin = table.push_input_stream(stdin).context("stdin")?;
-        let stdout = table.push_output_stream(stdout).context("stdout")?;
-        let stderr = table.push_output_stream(stderr).context("stderr")?;
+        let stdin_ix = table.push_input_stream(stdin.0).context("stdin")?;
+        let stdout_ix = table.push_output_stream(stdout.0).context("stdout")?;
+        let stderr_ix = table.push_output_stream(stderr.0).context("stderr")?;
 
         let preopens = preopens
             .into_iter()
@@ -223,9 +245,18 @@ impl WasiCtxBuilder {
             .collect::<anyhow::Result<Vec<_>>>()?;
 
         Ok(WasiCtx {
-            stdin,
-            stdout,
-            stderr,
+            stdin: StdioInput {
+                input_stream: stdin_ix,
+                isatty: stdin.1,
+            },
+            stdout: StdioOutput {
+                output_stream: stdout_ix,
+                isatty: stdout.1,
+            },
+            stderr: StdioOutput {
+                output_stream: stderr_ix,
+                isatty: stderr.1,
+            },
             env,
             args,
             preopens,
@@ -254,7 +285,7 @@ pub struct WasiCtx {
     pub(crate) env: Vec<(String, String)>,
     pub(crate) args: Vec<String>,
     pub(crate) preopens: Vec<(u32, String)>,
-    pub(crate) stdin: u32,
-    pub(crate) stdout: u32,
-    pub(crate) stderr: u32,
+    pub(crate) stdin: StdioInput,
+    pub(crate) stdout: StdioOutput,
+    pub(crate) stderr: StdioOutput,
 }
diff --git a/crates/wasi/src/preview2/host/env.rs b/crates/wasi/src/preview2/host/env.rs
index edb4d118d..e765dbd81 100644
--- a/crates/wasi/src/preview2/host/env.rs
+++ b/crates/wasi/src/preview2/host/env.rs
@@ -1,5 +1,4 @@
-use crate::preview2::bindings::cli_base::{environment, stderr, stdin, stdout};
-use crate::preview2::bindings::io::streams;
+use crate::preview2::bindings::cli::environment;
 use crate::preview2::WasiView;
 
 impl<T: WasiView> environment::Host for T {
@@ -9,22 +8,8 @@ impl<T: WasiView> environment::Host for T {
     fn get_arguments(&mut self) -> anyhow::Result<Vec<String>> {
         Ok(self.ctx().args.clone())
     }
-}
-
-impl<T: WasiView> stdin::Host for T {
-    fn get_stdin(&mut self) -> Result<streams::InputStream, anyhow::Error> {
-        Ok(self.ctx().stdin)
-    }
-}
-
-impl<T: WasiView> stdout::Host for T {
-    fn get_stdout(&mut self) -> Result<streams::OutputStream, anyhow::Error> {
-        Ok(self.ctx().stdout)
-    }
-}
-
-impl<T: WasiView> stderr::Host for T {
-    fn get_stderr(&mut self) -> Result<streams::OutputStream, anyhow::Error> {
-        Ok(self.ctx().stderr)
+    fn initial_cwd(&mut self) -> anyhow::Result<Option<String>> {
+        // FIXME: expose cwd in builder and save in ctx
+        Ok(None)
     }
 }
diff --git a/crates/wasi/src/preview2/host/exit.rs b/crates/wasi/src/preview2/host/exit.rs
index 24acff103..d95866a9a 100644
--- a/crates/wasi/src/preview2/host/exit.rs
+++ b/crates/wasi/src/preview2/host/exit.rs
@@ -1,4 +1,4 @@
-use crate::preview2::{bindings::cli_base::exit, I32Exit, WasiView};
+use crate::preview2::{bindings::cli::exit, I32Exit, WasiView};
 
 impl<T: WasiView> exit::Host for T {
     fn exit(&mut self, status: Result<(), ()>) -> anyhow::Result<()> {
diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs
index f86cec58f..f0144562e 100644
--- a/crates/wasi/src/preview2/mod.rs
+++ b/crates/wasi/src/preview2/mod.rs
@@ -36,6 +36,7 @@ pub use self::error::I32Exit;
 pub use self::filesystem::{DirPerms, FilePerms};
 pub use self::poll::{ClosureFuture, HostPollable, MakeFuture, PollableFuture, TablePollableExt};
 pub use self::random::{thread_rng, Deterministic};
+pub use self::stdio::{stderr, stdin, stdout, IsATTY, Stderr, Stdin, Stdout};
 pub use self::stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt};
 pub use self::table::{OccupiedEntry, Table, TableError};
 pub use cap_fs_ext::SystemTimeSpec;
@@ -122,11 +123,16 @@ pub mod bindings {
               import wasi:random/random
               import wasi:random/insecure
               import wasi:random/insecure-seed
-              import wasi:cli-base/environment
-              import wasi:cli-base/exit
-              import wasi:cli-base/stdin
-              import wasi:cli-base/stdout
-              import wasi:cli-base/stderr
+              import wasi:cli/environment
+              import wasi:cli/exit
+              import wasi:cli/stdin
+              import wasi:cli/stdout
+              import wasi:cli/stderr
+              import wasi:cli/terminal-input
+              import wasi:cli/terminal-output
+              import wasi:cli/terminal-stdin
+              import wasi:cli/terminal-stdout
+              import wasi:cli/terminal-stderr
             ",
         tracing: true,
         trappable_error_type: {
@@ -142,7 +148,7 @@ pub mod bindings {
         });
     }
 
-    pub use self::_internal_rest::wasi::{cli_base, random};
+    pub use self::_internal_rest::wasi::{cli, random};
     pub mod filesystem {
         pub use super::_internal_io::wasi::filesystem::types;
         pub use super::_internal_rest::wasi::filesystem::preopens;
diff --git a/crates/wasi/src/preview2/preview1.rs b/crates/wasi/src/preview2/preview1.rs
index 2478a197c..a2f957aa1 100644
--- a/crates/wasi/src/preview2/preview1.rs
+++ b/crates/wasi/src/preview2/preview1.rs
@@ -1,10 +1,13 @@
-use crate::preview2::bindings::cli_base::{stderr, stdin, stdout};
+use crate::preview2::bindings::cli::{
+    stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, terminal_stdin,
+    terminal_stdout,
+};
 use crate::preview2::bindings::clocks::{monotonic_clock, wall_clock};
 use crate::preview2::bindings::filesystem::{preopens, types as filesystem};
 use crate::preview2::bindings::io::streams;
 use crate::preview2::filesystem::TableFsExt;
 use crate::preview2::host::filesystem::TableReaddirExt;
-use crate::preview2::{bindings, TableError, WasiView};
+use crate::preview2::{bindings, IsATTY, TableError, WasiView};
 use anyhow::{anyhow, bail, Context};
 use std::borrow::Borrow;
 use std::cell::Cell;
@@ -36,9 +39,18 @@ struct File {
 
 #[derive(Clone, Debug)]
 enum Descriptor {
-    Stdin(streams::InputStream),
-    Stdout(streams::OutputStream),
-    Stderr(streams::OutputStream),
+    Stdin {
+        input_stream: streams::InputStream,
+        isatty: IsATTY,
+    },
+    Stdout {
+        output_stream: streams::OutputStream,
+        isatty: IsATTY,
+    },
+    Stderr {
+        output_stream: streams::OutputStream,
+        isatty: IsATTY,
+    },
     PreopenDirectory((filesystem::Descriptor, String)),
     File(File),
 }
@@ -71,30 +83,78 @@ impl DerefMut for Descriptors {
 impl Descriptors {
     /// Initializes [Self] using `preopens`
     fn new(
-        preopens: &mut (impl preopens::Host + stdin::Host + stdout::Host + stderr::Host + ?Sized),
+        host: &mut (impl preopens::Host
+                  + stdin::Host
+                  + stdout::Host
+                  + stderr::Host
+                  + terminal_stdin::Host
+                  + terminal_stdout::Host
+                  + terminal_stderr::Host
+                  + terminal_input::Host
+                  + terminal_output::Host
+                  + ?Sized),
     ) -> Result<Self, types::Error> {
-        let stdin = preopens
-            .get_stdin()
-            .context("failed to call `get-stdin`")
-            .map_err(types::Error::trap)?;
-        let stdout = preopens
-            .get_stdout()
-            .context("failed to call `get-stdout`")
-            .map_err(types::Error::trap)?;
-        let stderr = preopens
-            .get_stderr()
-            .context("failed to call `get-stderr`")
-            .map_err(types::Error::trap)?;
-        let directories = preopens
+        let mut descriptors = Self::default();
+        descriptors.push(Descriptor::Stdin {
+            input_stream: host
+                .get_stdin()
+                .context("failed to call `get-stdin`")
+                .map_err(types::Error::trap)?,
+            isatty: if let Some(term_in) = host
+                .get_terminal_stdin()
+                .context("failed to call `get-terminal-stdin`")
+                .map_err(types::Error::trap)?
+            {
+                host.drop_terminal_input(term_in)
+                    .context("failed to call `drop-terminal-input`")
+                    .map_err(types::Error::trap)?;
+                IsATTY::Yes
+            } else {
+                IsATTY::No
+            },
+        })?;
+        descriptors.push(Descriptor::Stdout {
+            output_stream: host
+                .get_stdout()
+                .context("failed to call `get-stdout`")
+                .map_err(types::Error::trap)?,
+            isatty: if let Some(term_out) = host
+                .get_terminal_stdout()
+                .context("failed to call `get-terminal-stdout`")
+                .map_err(types::Error::trap)?
+            {
+                host.drop_terminal_output(term_out)
+                    .context("failed to call `drop-terminal-output`")
+                    .map_err(types::Error::trap)?;
+                IsATTY::Yes
+            } else {
+                IsATTY::No
+            },
+        })?;
+        descriptors.push(Descriptor::Stderr {
+            output_stream: host
+                .get_stderr()
+                .context("failed to call `get-stderr`")
+                .map_err(types::Error::trap)?,
+            isatty: if let Some(term_out) = host
+                .get_terminal_stderr()
+                .context("failed to call `get-terminal-stderr`")
+                .map_err(types::Error::trap)?
+            {
+                host.drop_terminal_output(term_out)
+                    .context("failed to call `drop-terminal-output`")
+                    .map_err(types::Error::trap)?;
+                IsATTY::Yes
+            } else {
+                IsATTY::No
+            },
+        })?;
+
+        for dir in host
             .get_directories()
             .context("failed to call `get-directories`")
-            .map_err(types::Error::trap)?;
-
-        let mut descriptors = Self::default();
-        descriptors.push(Descriptor::Stdin(stdin))?;
-        descriptors.push(Descriptor::Stdout(stdout))?;
-        descriptors.push(Descriptor::Stderr(stderr))?;
-        for dir in directories {
+            .map_err(types::Error::trap)?
+        {
             descriptors.push(Descriptor::PreopenDirectory(dir))?;
         }
         Ok(descriptors)
@@ -232,7 +292,9 @@ impl<T: WasiPreview1View + ?Sized> Transaction<'_, T> {
             Some(Descriptor::File(file @ File { fd, .. })) if self.view.table().is_file(*fd) => {
                 Ok(file)
             }
-            Some(Descriptor::Stdin(..) | Descriptor::Stdout(..) | Descriptor::Stderr(..)) => {
+            Some(
+                Descriptor::Stdin { .. } | Descriptor::Stdout { .. } | Descriptor::Stderr { .. },
+            ) => {
                 // NOTE: legacy implementation returns SPIPE here
                 Err(types::Errno::Spipe.into())
             }
@@ -245,8 +307,10 @@ impl<T: WasiPreview1View + ?Sized> Transaction<'_, T> {
         match self.get_descriptor(fd)? {
             Descriptor::File(File { fd, .. }) => Ok(*fd),
             Descriptor::PreopenDirectory((fd, _)) => Ok(*fd),
-            Descriptor::Stdin(stream) => Ok(*stream),
-            Descriptor::Stdout(stream) | Descriptor::Stderr(stream) => Ok(*stream),
+            Descriptor::Stdin { input_stream, .. } => Ok(*input_stream),
+            Descriptor::Stdout { output_stream, .. } | Descriptor::Stderr { output_stream, .. } => {
+                Ok(*output_stream)
+            }
         }
     }
 
@@ -270,7 +334,16 @@ impl<T: WasiPreview1View + ?Sized> Transaction<'_, T> {
 }
 
 trait WasiPreview1ViewExt:
-    WasiPreview1View + preopens::Host + stdin::Host + stdout::Host + stderr::Host
+    WasiPreview1View
+    + preopens::Host
+    + stdin::Host
+    + stdout::Host
+    + stderr::Host
+    + terminal_input::Host
+    + terminal_output::Host
+    + terminal_stdin::Host
+    + terminal_stdout::Host
+    + terminal_stderr::Host
 {
     /// Lazily initializes [`WasiPreview1Adapter`] returned by [`WasiPreview1View::adapter_mut`]
     /// and returns [`Transaction`] on success
@@ -319,8 +392,8 @@ impl<T: WasiPreview1View + preopens::Host> WasiPreview1ViewExt for T {}
 
 pub fn add_to_linker<
     T: WasiPreview1View
-        + bindings::cli_base::environment::Host
-        + bindings::cli_base::exit::Host
+        + bindings::cli::environment::Host
+        + bindings::cli::exit::Host
         + bindings::filesystem::types::Host
         + bindings::filesystem::preopens::Host
         + bindings::sync_io::poll::poll::Host
@@ -453,6 +526,15 @@ impl TryFrom<filesystem::DescriptorType> for types::Filetype {
     }
 }
 
+impl From<IsATTY> for types::Filetype {
+    fn from(isatty: IsATTY) -> Self {
+        match isatty {
+            IsATTY::Yes => types::Filetype::CharacterDevice,
+            IsATTY::No => types::Filetype::Unknown,
+        }
+    }
+}
+
 impl From<filesystem::ErrorCode> for types::Errno {
     fn from(code: filesystem::ErrorCode) -> Self {
         match code {
@@ -634,8 +716,8 @@ fn first_non_empty_iovec<'a>(
 // stored in the WasiPreview1Adapter struct.
 impl<
         T: WasiPreview1View
-            + bindings::cli_base::environment::Host
-            + bindings::cli_base::exit::Host
+            + bindings::cli::environment::Host
+            + bindings::cli::exit::Host
             + bindings::filesystem::preopens::Host
             + bindings::filesystem::types::Host
             + bindings::poll::poll::Host
@@ -806,11 +888,13 @@ impl<
             .ok_or(types::Errno::Badf)?
             .clone();
         match desc {
-            Descriptor::Stdin(stream) => streams::Host::drop_input_stream(self, stream)
-                .await
-                .context("failed to call `drop-input-stream`"),
-            Descriptor::Stdout(stream) | Descriptor::Stderr(stream) => {
-                streams::Host::drop_output_stream(self, stream)
+            Descriptor::Stdin { input_stream, .. } => {
+                streams::Host::drop_input_stream(self, input_stream)
+                    .await
+                    .context("failed to call `drop-input-stream`")
+            }
+            Descriptor::Stdout { output_stream, .. } | Descriptor::Stderr { output_stream, .. } => {
+                streams::Host::drop_output_stream(self, output_stream)
                     .await
                     .context("failed to call `drop-output-stream`")
             }
@@ -839,19 +923,19 @@ impl<
     #[instrument(skip(self))]
     async fn fd_fdstat_get(&mut self, fd: types::Fd) -> Result<types::Fdstat, types::Error> {
         let (fd, blocking, append) = match self.transact()?.get_descriptor(fd)? {
-            Descriptor::Stdin(..) => {
+            Descriptor::Stdin { isatty, .. } => {
                 let fs_rights_base = types::Rights::FD_READ;
                 return Ok(types::Fdstat {
-                    fs_filetype: types::Filetype::CharacterDevice,
+                    fs_filetype: (*isatty).into(),
                     fs_flags: types::Fdflags::empty(),
                     fs_rights_base,
                     fs_rights_inheriting: fs_rights_base,
                 });
             }
-            Descriptor::Stdout(..) | Descriptor::Stderr(..) => {
+            Descriptor::Stdout { isatty, .. } | Descriptor::Stderr { isatty, .. } => {
                 let fs_rights_base = types::Rights::FD_WRITE;
                 return Ok(types::Fdstat {
-                    fs_filetype: types::Filetype::CharacterDevice,
+                    fs_filetype: (*isatty).into(),
                     fs_flags: types::Fdflags::empty(),
                     fs_rights_base,
                     fs_rights_inheriting: fs_rights_base,
@@ -996,18 +1080,18 @@ impl<
     async fn fd_filestat_get(&mut self, fd: types::Fd) -> Result<types::Filestat, types::Error> {
         let desc = self.transact()?.get_descriptor(fd)?.clone();
         match desc {
-            Descriptor::Stdin(..) | Descriptor::Stdout(..) | Descriptor::Stderr(..) => {
-                Ok(types::Filestat {
-                    dev: 0,
-                    ino: 0,
-                    filetype: types::Filetype::CharacterDevice,
-                    nlink: 0,
-                    size: 0,
-                    atim: 0,
-                    mtim: 0,
-                    ctim: 0,
-                })
-            }
+            Descriptor::Stdin { isatty, .. }
+            | Descriptor::Stdout { isatty, .. }
+            | Descriptor::Stderr { isatty, .. } => Ok(types::Filestat {
+                dev: 0,
+                ino: 0,
+                filetype: isatty.into(),
+                nlink: 0,
+                size: 0,
+                atim: 0,
+                mtim: 0,
+                ctim: 0,
+            }),
             Descriptor::PreopenDirectory((fd, _)) | Descriptor::File(File { fd, .. }) => {
                 let filesystem::DescriptorStat {
                     type_,
@@ -1129,14 +1213,17 @@ impl<
 
                 (buf, read, state)
             }
-            Descriptor::Stdin(stream) => {
+            Descriptor::Stdin { input_stream, .. } => {
                 let Some(buf) = first_non_empty_iovec(iovs)? else {
                     return Ok(0)
                 };
-                let (read, state) =
-                    streams::Host::read(self, stream, buf.len().try_into().unwrap_or(u64::MAX))
-                        .await
-                        .map_err(|_| types::Errno::Io)?;
+                let (read, state) = streams::Host::read(
+                    self,
+                    input_stream,
+                    buf.len().try_into().unwrap_or(u64::MAX),
+                )
+                .await
+                .map_err(|_| types::Errno::Io)?;
                 (buf, read, state)
             }
             _ => return Err(types::Errno::Badf.into()),
@@ -1184,7 +1271,7 @@ impl<
 
                 (buf, read, state)
             }
-            Descriptor::Stdin(..) => {
+            Descriptor::Stdin { .. } => {
                 // NOTE: legacy implementation returns SPIPE here
                 return Err(types::Errno::Spipe.into());
             }
@@ -1249,11 +1336,11 @@ impl<
                 }
                 n
             }
-            Descriptor::Stdout(stream) | Descriptor::Stderr(stream) => {
+            Descriptor::Stdout { output_stream, .. } | Descriptor::Stderr { output_stream, .. } => {
                 let Some(buf) = first_non_empty_ciovec(ciovs)? else {
                     return Ok(0)
                 };
-                let (n, _stat) = streams::Host::blocking_write(self, stream, buf)
+                let (n, _stat) = streams::Host::blocking_write(self, output_stream, buf)
                     .await
                     .map_err(|_| types::Errno::Io)?;
                 n
@@ -1291,7 +1378,7 @@ impl<
                 }
                 .map_err(|_| types::Errno::Io)?
             }
-            Descriptor::Stdout(..) | Descriptor::Stderr(..) => {
+            Descriptor::Stdout { .. } | Descriptor::Stderr { .. } => {
                 // NOTE: legacy implementation returns SPIPE here
                 return Err(types::Errno::Spipe.into());
             }
diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs
index 81083d2e0..947a4885f 100644
--- a/crates/wasi/src/preview2/stdio.rs
+++ b/crates/wasi/src/preview2/stdio.rs
@@ -1,4 +1,13 @@
+use crate::preview2::bindings::cli::{
+    stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, terminal_stdin,
+    terminal_stdout,
+};
+use crate::preview2::bindings::io::streams;
 use crate::preview2::pipe::AsyncWriteStream;
+use crate::preview2::{HostOutputStream, StreamState, WasiView};
+use anyhow::Error;
+use bytes::Bytes;
+use is_terminal::IsTerminal;
 
 #[cfg(unix)]
 mod unix;
@@ -10,15 +19,121 @@ mod worker_thread_stdin;
 #[cfg(windows)]
 pub use self::worker_thread_stdin::{stdin, Stdin};
 
-pub type Stdout = AsyncWriteStream;
+pub struct Stdout(AsyncWriteStream);
 
 pub fn stdout() -> Stdout {
-    AsyncWriteStream::new(tokio::io::stdout())
+    Stdout(AsyncWriteStream::new(tokio::io::stdout()))
 }
-pub type Stderr = AsyncWriteStream;
+impl IsTerminal for Stdout {
+    fn is_terminal(&self) -> bool {
+        std::io::stdout().is_terminal()
+    }
+}
+#[async_trait::async_trait]
+impl HostOutputStream for Stdout {
+    fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error> {
+        self.0.write(bytes)
+    }
+    async fn ready(&mut self) -> Result<(), Error> {
+        self.0.ready().await
+    }
+}
+
+pub struct Stderr(AsyncWriteStream);
 
 pub fn stderr() -> Stderr {
-    AsyncWriteStream::new(tokio::io::stderr())
+    Stderr(AsyncWriteStream::new(tokio::io::stderr()))
+}
+impl IsTerminal for Stderr {
+    fn is_terminal(&self) -> bool {
+        std::io::stderr().is_terminal()
+    }
+}
+#[async_trait::async_trait]
+impl HostOutputStream for Stderr {
+    fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error> {
+        self.0.write(bytes)
+    }
+    async fn ready(&mut self) -> Result<(), Error> {
+        self.0.ready().await
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum IsATTY {
+    Yes,
+    No,
+}
+
+pub(crate) struct StdioInput {
+    pub input_stream: streams::InputStream,
+    pub isatty: IsATTY,
+}
+
+pub(crate) struct StdioOutput {
+    pub output_stream: streams::OutputStream,
+    pub isatty: IsATTY,
+}
+
+impl<T: WasiView> stdin::Host for T {
+    fn get_stdin(&mut self) -> Result<streams::InputStream, anyhow::Error> {
+        Ok(self.ctx().stdin.input_stream)
+    }
+}
+
+impl<T: WasiView> stdout::Host for T {
+    fn get_stdout(&mut self) -> Result<streams::OutputStream, anyhow::Error> {
+        Ok(self.ctx().stdout.output_stream)
+    }
+}
+
+impl<T: WasiView> stderr::Host for T {
+    fn get_stderr(&mut self) -> Result<streams::OutputStream, anyhow::Error> {
+        Ok(self.ctx().stderr.output_stream)
+    }
+}
+
+struct HostTerminalInput;
+struct HostTerminalOutput;
+
+impl<T: WasiView> terminal_input::Host for T {
+    fn drop_terminal_input(&mut self, r: terminal_input::TerminalInput) -> anyhow::Result<()> {
+        self.table_mut().delete::<HostTerminalInput>(r)?;
+        Ok(())
+    }
+}
+impl<T: WasiView> terminal_output::Host for T {
+    fn drop_terminal_output(&mut self, r: terminal_output::TerminalOutput) -> anyhow::Result<()> {
+        self.table_mut().delete::<HostTerminalOutput>(r)?;
+        Ok(())
+    }
+}
+impl<T: WasiView> terminal_stdin::Host for T {
+    fn get_terminal_stdin(&mut self) -> anyhow::Result<Option<terminal_input::TerminalInput>> {
+        if let IsATTY::Yes = self.ctx().stdin.isatty {
+            Ok(Some(self.table_mut().push(Box::new(HostTerminalInput))?))
+        } else {
+            Ok(None)
+        }
+    }
+}
+impl<T: WasiView> terminal_stdout::Host for T {
+    fn get_terminal_stdout(&mut self) -> anyhow::Result<Option<terminal_output::TerminalOutput>> {
+        if let IsATTY::Yes = self.ctx().stdout.isatty {
+            Ok(Some(self.table_mut().push(Box::new(HostTerminalOutput))?))
+        } else {
+            Ok(None)
+        }
+    }
+}
+impl<T: WasiView> terminal_stderr::Host for T {
+    fn get_terminal_stderr(&mut self) -> anyhow::Result<Option<terminal_output::TerminalOutput>> {
+        if let IsATTY::Yes = self.ctx().stderr.isatty {
+            Ok(Some(self.table_mut().push(Box::new(HostTerminalOutput))?))
+        } else {
+            Ok(None)
+        }
+    }
 }
 
 #[cfg(all(unix, test))]
diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs
index b624b874e..a27cf381d 100644
--- a/crates/wasi/src/preview2/stdio/unix.rs
+++ b/crates/wasi/src/preview2/stdio/unix.rs
@@ -65,6 +65,12 @@ pub fn stdin() -> Stdin {
     handle
 }
 
+impl is_terminal::IsTerminal for Stdin {
+    fn is_terminal(&self) -> bool {
+        std::io::stdin().is_terminal()
+    }
+}
+
 #[async_trait::async_trait]
 impl crate::preview2::HostInputStream for Stdin {
     fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> {
diff --git a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs
index 116c747ca..c5be39b6d 100644
--- a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs
+++ b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs
@@ -91,6 +91,12 @@ pub fn stdin() -> Stdin {
     Stdin
 }
 
+impl is_terminal::IsTerminal for Stdin {
+    fn is_terminal(&self) -> bool {
+        std::io::stdin().is_terminal()
+    }
+}
+
 #[async_trait::async_trait]
 impl HostInputStream for Stdin {
     fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> {
diff --git a/crates/wasi/wit/deps/preview/command.wit b/crates/wasi/wit/deps/cli/command.wit
similarity index 70%
rename from crates/wasi/wit/deps/preview/command.wit
rename to crates/wasi/wit/deps/cli/command.wit
index dcd06d795..c29b4a61f 100644
--- a/crates/wasi/wit/deps/preview/command.wit
+++ b/crates/wasi/wit/deps/cli/command.wit
@@ -1,3 +1,5 @@
+package wasi:cli
+
 world command {
   import wasi:clocks/wall-clock
   import wasi:clocks/monotonic-clock
@@ -16,11 +18,15 @@ world command {
   import wasi:random/insecure-seed
   import wasi:poll/poll
   import wasi:io/streams
-  import wasi:cli-base/environment
-  import wasi:cli-base/exit
-  import wasi:cli-base/stdin
-  import wasi:cli-base/stdout
-  import wasi:cli-base/stderr
-
-  export run: func() -> result
+  import environment
+  import exit
+  import stdin
+  import stdout
+  import stderr
+  import terminal-input
+  import terminal-output
+  import terminal-stdin
+  import terminal-stdout
+  import terminal-stderr
+  export run
 }
diff --git a/crates/wasi/wit/deps/wasi-cli-base/environment.wit b/crates/wasi/wit/deps/cli/environment.wit
similarity index 75%
rename from crates/wasi/wit/deps/wasi-cli-base/environment.wit
rename to crates/wasi/wit/deps/cli/environment.wit
index 4c97c85d1..36790fe71 100644
--- a/crates/wasi/wit/deps/wasi-cli-base/environment.wit
+++ b/crates/wasi/wit/deps/cli/environment.wit
@@ -1,5 +1,3 @@
-package wasi:cli-base
-
 interface environment {
   /// Get the POSIX-style environment variables.
   ///
@@ -13,4 +11,8 @@ interface environment {
 
   /// Get the POSIX-style arguments to the program.
   get-arguments: func() -> list<string>
+
+  /// Return a path that programs should use as their initial current working
+  /// directory, interpreting `.` as shorthand for this.
+  initial-cwd: func() -> option<string>
 }
diff --git a/crates/wasi/wit/deps/cli/exit.wit b/crates/wasi/wit/deps/cli/exit.wit
new file mode 100644
index 000000000..4831d5078
--- /dev/null
+++ b/crates/wasi/wit/deps/cli/exit.wit
@@ -0,0 +1,4 @@
+interface exit {
+  /// Exit the current instance and any linked instances.
+  exit: func(status: result)
+}
diff --git a/crates/wasi/wit/deps/cli/run.wit b/crates/wasi/wit/deps/cli/run.wit
new file mode 100644
index 000000000..45a1ca533
--- /dev/null
+++ b/crates/wasi/wit/deps/cli/run.wit
@@ -0,0 +1,4 @@
+interface run {
+  /// Run the program.
+  run: func() -> result
+}
diff --git a/crates/wasi/wit/deps/wasi-cli-base/stdio.wit b/crates/wasi/wit/deps/cli/stdio.wit
similarity index 100%
rename from crates/wasi/wit/deps/wasi-cli-base/stdio.wit
rename to crates/wasi/wit/deps/cli/stdio.wit
diff --git a/crates/wasi/wit/deps/cli/terminal.wit b/crates/wasi/wit/deps/cli/terminal.wit
new file mode 100644
index 000000000..f32e74437
--- /dev/null
+++ b/crates/wasi/wit/deps/cli/terminal.wit
@@ -0,0 +1,59 @@
+interface terminal-input {
+    /// The input side of a terminal.
+    ///
+    /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources).
+    type terminal-input = u32
+
+    // In the future, this may include functions for disabling echoing,
+    // disabling input buffering so that keyboard events are sent through
+    // immediately, querying supported features, and so on.
+
+    /// Dispose of the specified terminal-input after which it may no longer
+    /// be used.
+    drop-terminal-input: func(this: terminal-input)
+}
+
+interface terminal-output {
+    /// The output side of a terminal.
+    ///
+    /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources).
+    type terminal-output = u32
+
+    // In the future, this may include functions for querying the terminal
+    // size, being notified of terminal size changes, querying supported
+    // features, and so on.
+
+    /// Dispose of the specified terminal-output, after which it may no longer
+    /// be used.
+    drop-terminal-output: func(this: terminal-output)
+}
+
+/// An interface providing an optional `terminal-input` for stdin as a
+/// link-time authority.
+interface terminal-stdin {
+    use terminal-input.{terminal-input}
+
+    /// If stdin is connected to a terminal, return a `terminal-input` handle
+    /// allowing further interaction with it.
+    get-terminal-stdin: func() -> option<terminal-input>
+}
+
+/// An interface providing an optional `terminal-output` for stdout as a
+/// link-time authority.
+interface terminal-stdout {
+    use terminal-output.{terminal-output}
+
+    /// If stdout is connected to a terminal, return a `terminal-output` handle
+    /// allowing further interaction with it.
+    get-terminal-stdout: func() -> option<terminal-output>
+}
+
+/// An interface providing an optional `terminal-output` for stderr as a
+/// link-time authority.
+interface terminal-stderr {
+    use terminal-output.{terminal-output}
+
+    /// If stderr is connected to a terminal, return a `terminal-output` handle
+    /// allowing further interaction with it.
+    get-terminal-stderr: func() -> option<terminal-output>
+}
diff --git a/crates/wasi/wit/deps/preview/command-extended.wit b/crates/wasi/wit/deps/preview/command-extended.wit
deleted file mode 100644
index 8fe716038..000000000
--- a/crates/wasi/wit/deps/preview/command-extended.wit
+++ /dev/null
@@ -1,36 +0,0 @@
-package wasi:preview
-
-world command-extended {
-  import wasi:clocks/wall-clock
-  import wasi:clocks/monotonic-clock
-  import wasi:clocks/timezone
-  import wasi:filesystem/types
-  import wasi:filesystem/preopens
-  import wasi:sockets/instance-network
-  import wasi:sockets/ip-name-lookup
-  import wasi:sockets/network
-  import wasi:sockets/tcp-create-socket
-  import wasi:sockets/tcp
-  import wasi:sockets/udp-create-socket
-  import wasi:sockets/udp
-  import wasi:random/random
-  import wasi:random/insecure
-  import wasi:random/insecure-seed
-  import wasi:poll/poll
-  import wasi:io/streams
-  import wasi:cli-base/environment
-  import wasi:cli-base/exit
-  import wasi:cli-base/stdin
-  import wasi:cli-base/stdout
-  import wasi:cli-base/stderr
-
-  // We should replace all others with `include self.command`
-  // as soon as the unioning of worlds is available:
-  // https://github.com/WebAssembly/component-model/issues/169
-  import wasi:logging/handler
-  import wasi:http/outgoing-handler
-
-  export run: func(
-    args: list<string>,
-  ) -> result
-}
diff --git a/crates/wasi/wit/deps/preview/proxy.wit b/crates/wasi/wit/deps/preview/proxy.wit
deleted file mode 100644
index a616daa1c..000000000
--- a/crates/wasi/wit/deps/preview/proxy.wit
+++ /dev/null
@@ -1,9 +0,0 @@
-world proxy {
-  import wasi:random/random
-  import wasi:random/insecure
-  import wasi:random/insecure-seed
-  import wasi:logging/handler
-  import wasi:http/outgoing-handler
-
-  export wasi:http/incoming-handler
-}
diff --git a/crates/wasi/wit/deps/preview/reactor.wit b/crates/wasi/wit/deps/preview/reactor.wit
deleted file mode 100644
index aff49ea4d..000000000
--- a/crates/wasi/wit/deps/preview/reactor.wit
+++ /dev/null
@@ -1,24 +0,0 @@
-world reactor {
-  import wasi:clocks/wall-clock
-  import wasi:clocks/monotonic-clock
-  import wasi:clocks/timezone
-  import wasi:filesystem/types
-  import wasi:filesystem/preopens
-  import wasi:sockets/instance-network
-  import wasi:sockets/ip-name-lookup
-  import wasi:sockets/network
-  import wasi:sockets/tcp-create-socket
-  import wasi:sockets/tcp
-  import wasi:sockets/udp-create-socket
-  import wasi:sockets/udp
-  import wasi:random/random
-  import wasi:poll/poll
-  import wasi:io/streams
-  import wasi:logging/handler
-  import wasi:http/outgoing-handler
-  import wasi:cli-base/environment
-  import wasi:cli-base/exit
-  import wasi:cli-base/stdin
-  import wasi:cli-base/stdout
-  import wasi:cli-base/stderr
-}
diff --git a/crates/wasi/wit/deps/wasi-cli-base/exit.wit b/crates/wasi/wit/deps/wasi-cli-base/exit.wit
deleted file mode 100644
index 66835aa70..000000000
--- a/crates/wasi/wit/deps/wasi-cli-base/exit.wit
+++ /dev/null
@@ -1,4 +0,0 @@
-interface exit {
-  /// Exit the curerent instance and any linked instances.
-  exit: func(status: result)
-}
diff --git a/crates/wasi/wit/main.wit b/crates/wasi/wit/main.wit
index 264dfee82..753770ad2 100644
--- a/crates/wasi/wit/main.wit
+++ b/crates/wasi/wit/main.wit
@@ -1,1 +1,33 @@
-package unused:main
+package wasmtime:wasi
+
+// All of the same imports available in the wasi:cli/command world, but no
+// export required:
+world preview1-adapter-reactor {
+  import wasi:clocks/wall-clock
+  import wasi:clocks/monotonic-clock
+  import wasi:clocks/timezone
+  import wasi:filesystem/types
+  import wasi:filesystem/preopens
+  import wasi:sockets/instance-network
+  import wasi:sockets/ip-name-lookup
+  import wasi:sockets/network
+  import wasi:sockets/tcp-create-socket
+  import wasi:sockets/tcp
+  import wasi:sockets/udp-create-socket
+  import wasi:sockets/udp
+  import wasi:random/random
+  import wasi:random/insecure
+  import wasi:random/insecure-seed
+  import wasi:poll/poll
+  import wasi:io/streams
+  import wasi:cli/environment
+  import wasi:cli/exit
+  import wasi:cli/stdin
+  import wasi:cli/stdout
+  import wasi:cli/stderr
+  import wasi:cli/terminal-input
+  import wasi:cli/terminal-output
+  import wasi:cli/terminal-stdin
+  import wasi:cli/terminal-stdout
+  import wasi:cli/terminal-stderr
+}


Test patch:
diff --git a/crates/test-programs/Cargo.toml b/crates/test-programs/Cargo.toml
index 37a97b40b..bd9de1569 100644
--- a/crates/test-programs/Cargo.toml
+++ b/crates/test-programs/Cargo.toml
@@ -13,6 +13,8 @@ cargo_metadata = "0.15.3"
 wit-component = { workspace = true }
 heck = { workspace = true }
 
+[dependencies]
+is-terminal = { workspace = true }
 
 [dev-dependencies]
 anyhow = { workspace = true }
diff --git a/crates/test-programs/command-tests/src/bin/stream_pollable_lifetimes.rs b/crates/test-programs/command-tests/src/bin/stream_pollable_lifetimes.rs
index 1ccc71064..ebd807195 100644
--- a/crates/test-programs/command-tests/src/bin/stream_pollable_lifetimes.rs
+++ b/crates/test-programs/command-tests/src/bin/stream_pollable_lifetimes.rs
@@ -1,5 +1,5 @@
-use command_tests::wasi::cli_base::environment;
-use command_tests::wasi::cli_base::stdin;
+use command_tests::wasi::cli::environment;
+use command_tests::wasi::cli::stdin;
 use command_tests::wasi::io::streams;
 use command_tests::wasi::poll::poll;
 
diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs
index 960fd9a7a..11eca7f28 100644
--- a/crates/test-programs/src/lib.rs
+++ b/crates/test-programs/src/lib.rs
@@ -35,3 +35,10 @@ pub fn wasi_tests_environment() -> &'static [(&'static str, &'static str)] {
         ]
     }
 }
+
+pub fn stdio_is_terminal() -> bool {
+    use is_terminal::is_terminal;
+    is_terminal(&std::io::stdin())
+        && is_terminal(&std::io::stdout())
+        && is_terminal(&std::io::stderr())
+}
diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs
index 7b5877e9d..574380420 100644
--- a/crates/test-programs/tests/command.rs
+++ b/crates/test-programs/tests/command.rs
@@ -8,7 +8,7 @@ use wasmtime::{
 use wasmtime_wasi::preview2::{
     command::{add_to_linker, Command},
     pipe::MemoryInputPipe,
-    DirPerms, FilePerms, HostMonotonicClock, HostWallClock, Table, WasiCtx, WasiCtxBuilder,
+    DirPerms, FilePerms, HostMonotonicClock, HostWallClock, IsATTY, Table, WasiCtx, WasiCtxBuilder,
     WasiView,
 };
 
@@ -68,6 +68,7 @@ async fn hello_stdout() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("hello_stdout"), CommandCtx { table, wasi }).await?;
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -90,7 +91,7 @@ async fn panic() -> Result<()> {
         .build(&mut table)?;
     let (mut store, command) =
         instantiate(get_component("panic"), CommandCtx { table, wasi }).await?;
-    let r = command.call_run(&mut store).await;
+    let r = command.wasi_cli_run().call_run(&mut store).await;
     assert!(r.is_err());
     println!("{:?}", r);
     Ok(())
@@ -105,6 +106,7 @@ async fn args() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("args"), CommandCtx { table, wasi }).await?;
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -118,6 +120,7 @@ async fn random() -> Result<()> {
         instantiate(get_component("random"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -164,6 +167,7 @@ async fn time() -> Result<()> {
         instantiate(get_component("time"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -173,15 +177,17 @@ async fn time() -> Result<()> {
 async fn stdin() -> Result<()> {
     let mut table = Table::new();
     let wasi = WasiCtxBuilder::new()
-        .stdin(MemoryInputPipe::new(
-            "So rested he by the Tumtum tree".into(),
-        ))
+        .stdin(
+            MemoryInputPipe::new("So rested he by the Tumtum tree".into()),
+            IsATTY::No,
+        )
         .build(&mut table)?;
 
     let (mut store, command) =
         instantiate(get_component("stdin"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -191,15 +197,17 @@ async fn stdin() -> Result<()> {
 async fn poll_stdin() -> Result<()> {
     let mut table = Table::new();
     let wasi = WasiCtxBuilder::new()
-        .stdin(MemoryInputPipe::new(
-            "So rested he by the Tumtum tree".into(),
-        ))
+        .stdin(
+            MemoryInputPipe::new("So rested he by the Tumtum tree".into()),
+            IsATTY::No,
+        )
         .build(&mut table)?;
 
     let (mut store, command) =
         instantiate(get_component("poll_stdin"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -217,6 +225,7 @@ async fn env() -> Result<()> {
         instantiate(get_component("env"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -239,6 +248,7 @@ async fn file_read() -> Result<()> {
         instantiate(get_component("file_read"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -261,6 +271,7 @@ async fn file_append() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("file_append"), CommandCtx { table, wasi }).await?;
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))?;
@@ -294,6 +305,7 @@ async fn file_dir_sync() -> Result<()> {
         instantiate(get_component("file_dir_sync"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -307,7 +319,7 @@ async fn exit_success() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("exit_success"), CommandCtx { table, wasi }).await?;
 
-    let r = command.call_run(&mut store).await;
+    let r = command.wasi_cli_run().call_run(&mut store).await;
     let err = r.unwrap_err();
     let status = err
         .downcast_ref::<wasmtime_wasi::preview2::I32Exit>()
@@ -324,7 +336,7 @@ async fn exit_default() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("exit_default"), CommandCtx { table, wasi }).await?;
 
-    let r = command.call_run(&mut store).await?;
+    let r = command.wasi_cli_run().call_run(&mut store).await?;
     assert!(r.is_ok());
     Ok(())
 }
@@ -337,7 +349,7 @@ async fn exit_failure() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("exit_failure"), CommandCtx { table, wasi }).await?;
 
-    let r = command.call_run(&mut store).await;
+    let r = command.wasi_cli_run().call_run(&mut store).await;
     let err = r.unwrap_err();
     let status = err
         .downcast_ref::<wasmtime_wasi::preview2::I32Exit>()
@@ -354,7 +366,7 @@ async fn exit_panic() -> Result<()> {
     let (mut store, command) =
         instantiate(get_component("exit_panic"), CommandCtx { table, wasi }).await?;
 
-    let r = command.call_run(&mut store).await;
+    let r = command.wasi_cli_run().call_run(&mut store).await;
     let err = r.unwrap_err();
     // The panic should trap.
     assert!(err
@@ -387,6 +399,7 @@ async fn directory_list() -> Result<()> {
         instantiate(get_component("directory_list"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -401,6 +414,7 @@ async fn default_clocks() -> Result<()> {
         instantiate(get_component("default_clocks"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -417,6 +431,7 @@ async fn export_cabi_realloc() -> Result<()> {
     .await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -439,6 +454,7 @@ async fn read_only() -> Result<()> {
         instantiate(get_component("read_only"), CommandCtx { table, wasi }).await?;
 
     command
+        .wasi_cli_run()
         .call_run(&mut store)
         .await?
         .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))
@@ -452,7 +468,7 @@ async fn stream_pollable_lifetimes() -> Result<()> {
         let mut table = Table::new();
         let wasi = WasiCtxBuilder::new()
             .args(&["correct"])
-            .stdin(MemoryInputPipe::new(" ".into()))
+            .stdin(MemoryInputPipe::new(" ".into()), IsATTY::No)
             .build(&mut table)?;
 
         let (mut store, command) = instantiate(
@@ -462,6 +478,7 @@ async fn stream_pollable_lifetimes() -> Result<()> {
         .await?;
 
         command
+            .wasi_cli_run()
             .call_run(&mut store)
             .await?
             .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))?;
@@ -471,7 +488,7 @@ async fn stream_pollable_lifetimes() -> Result<()> {
         let mut table = Table::new();
         let wasi = WasiCtxBuilder::new()
             .args(&["trap"])
-            .stdin(MemoryInputPipe::new(" ".into()))
+            .stdin(MemoryInputPipe::new(" ".into()), IsATTY::No)
             .build(&mut table)?;
 
         let (mut store, command) = instantiate(
@@ -481,6 +498,7 @@ async fn stream_pollable_lifetimes() -> Result<()> {
         .await?;
 
         let trap = command
+            .wasi_cli_run()
             .call_run(&mut store)
             .await
             .err()
diff --git a/crates/test-programs/tests/reactor.rs b/crates/test-programs/tests/reactor.rs
index b9e4c56ca..7dff956b6 100644
--- a/crates/test-programs/tests/reactor.rs
+++ b/crates/test-programs/tests/reactor.rs
@@ -30,11 +30,16 @@ wasmtime::component::bindgen!({
        "wasi:io/streams": preview2::bindings::io::streams,
        "wasi:filesystem/types": preview2::bindings::filesystem::types,
        "wasi:filesystem/preopens": preview2::bindings::filesystem::preopens,
-       "wasi:cli-base/environment": preview2::bindings::cli_base::environment,
-       "wasi:cli-base/exit": preview2::bindings::cli_base::exit,
-       "wasi:cli-base/stdin": preview2::bindings::cli_base::stdin,
-       "wasi:cli-base/stdout": preview2::bindings::cli_base::stdout,
-       "wasi:cli-base/stderr": preview2::bindings::cli_base::stderr,
+       "wasi:cli/environment": preview2::bindings::cli::environment,
+       "wasi:cli/exit": preview2::bindings::cli::exit,
+       "wasi:cli/stdin": preview2::bindings::cli::stdin,
+       "wasi:cli/stdout": preview2::bindings::cli::stdout,
+       "wasi:cli/stderr": preview2::bindings::cli::stderr,
+       "wasi:cli/terminal_input": preview2::bindings::cli::terminal_input,
+       "wasi:cli/terminal_output": preview2::bindings::cli::terminal_output,
+       "wasi:cli/terminal_stdin": preview2::bindings::cli::terminal_stdin,
+       "wasi:cli/terminal_stdout": preview2::bindings::cli::terminal_stdout,
+       "wasi:cli/terminal_stderr": preview2::bindings::cli::terminal_stderr,
     },
     ownership: Borrowing {
         duplicate_if_necessary: false
@@ -71,11 +76,16 @@ async fn instantiate(
     preview2::bindings::filesystem::types::add_to_linker(&mut linker, |x| x)?;
     preview2::bindings::filesystem::preopens::add_to_linker(&mut linker, |x| x)?;
     preview2::bindings::io::streams::add_to_linker(&mut linker, |x| x)?;
-    preview2::bindings::cli_base::environment::add_to_linker(&mut linker, |x| x)?;
-    preview2::bindings::cli_base::exit::add_to_linker(&mut linker, |x| x)?;
-    preview2::bindings::cli_base::stdin::add_to_linker(&mut linker, |x| x)?;
-    preview2::bindings::cli_base::stdout::add_to_linker(&mut linker, |x| x)?;
-    preview2::bindings::cli_base::stderr::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::environment::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::exit::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::stdin::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::stdout::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::stderr::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::terminal_input::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::terminal_output::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::terminal_stdin::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::terminal_stdout::add_to_linker(&mut linker, |x| x)?;
+    preview2::bindings::cli::terminal_stderr::add_to_linker(&mut linker, |x| x)?;
 
     let mut store = Store::new(&ENGINE, wasi_ctx);
 
diff --git a/crates/test-programs/tests/wasi-cap-std-sync.rs b/crates/test-programs/tests/wasi-cap-std-sync.rs
index f373b33df..6b7143f3b 100644
--- a/crates/test-programs/tests/wasi-cap-std-sync.rs
+++ b/crates/test-programs/tests/wasi-cap-std-sync.rs
@@ -160,8 +160,8 @@ fn interesting_paths() {
     run("interesting_paths", true).unwrap()
 }
 #[test_log::test]
-fn isatty() {
-    run("isatty", true).unwrap()
+fn regular_file_isatty() {
+    run("regular_file_isatty", true).unwrap()
 }
 #[test_log::test]
 fn nofollow_errors() {
@@ -253,6 +253,19 @@ fn sched_yield() {
 fn stdio() {
     run("stdio", true).unwrap()
 }
+#[test_log::test]
+fn stdio_isatty() {
+    if test_programs::stdio_is_terminal() {
+        // Inherit stdio, which is a terminal in the test runner's environment:
+        run("stdio_isatty", true).unwrap()
+    }
+}
+#[test_log::test]
+fn stdio_not_isatty() {
+    // Don't inherit stdio, test asserts each is not tty:
+    run("stdio_not_isatty", false).unwrap()
+}
+
 #[test_log::test]
 fn symlink_create() {
     run("symlink_create", true).unwrap()
diff --git a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs
index 700cfd234..064620b0d 100644
--- a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs
+++ b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs
@@ -5,7 +5,7 @@ use wasmtime::{Config, Engine, Linker, Store};
 use wasmtime_wasi::preview2::{
     pipe::MemoryOutputPipe,
     preview1::{add_to_linker, WasiPreview1Adapter, WasiPreview1View},
-    DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView,
+    DirPerms, FilePerms, IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView,
 };
 
 lazy_static::lazy_static! {
@@ -43,7 +43,9 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> {
         if inherit_stdio {
             builder.inherit_stdio();
         } else {
-            builder.stdout(stdout.clone()).stderr(stderr.clone());
+            builder
+                .stdout(stdout.clone(), IsATTY::No)
+                .stderr(stderr.clone(), IsATTY::No);
         }
         builder.args(&[name, "."]);
         println!("preopen: {:?}", workspace);
@@ -193,8 +195,8 @@ async fn interesting_paths() {
     run("interesting_paths", false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
-async fn isatty() {
-    run("isatty", false).await.unwrap()
+async fn regular_file_isatty() {
+    run("regular_file_isatty", true).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn nofollow_errors() {
@@ -296,6 +298,19 @@ async fn stdio() {
     run("stdio", false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn stdio_isatty() {
+    // If the test process is setup such that stdio is a terminal:
+    if test_programs::stdio_is_terminal() {
+        // Inherit stdio, test asserts each is a tty:
+        run("stdio_isatty", true).await.unwrap()
+    }
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn stdio_not_isatty() {
+    // Don't inherit stdio, test asserts each is not tty:
+    run("stdio_not_isatty", false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn symlink_create() {
     run("symlink_create", false).await.unwrap()
 }
diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs
index b618b9f40..08b60217b 100644
--- a/crates/test-programs/tests/wasi-preview2-components-sync.rs
+++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs
@@ -5,7 +5,7 @@ use wasmtime::{component::Linker, Config, Engine, Store};
 use wasmtime_wasi::preview2::{
     command::sync::{add_to_linker, Command},
     pipe::MemoryOutputPipe,
-    DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView,
+    DirPerms, FilePerms, IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView,
 };
 
 lazy_static::lazy_static! {
@@ -43,7 +43,9 @@ fn run(name: &str, inherit_stdio: bool) -> Result<()> {
         if inherit_stdio {
             builder.inherit_stdio();
         } else {
-            builder.stdout(stdout.clone()).stderr(stderr.clone());
+            builder
+                .stdout(stdout.clone(), IsATTY::No)
+                .stderr(stderr.clone(), IsATTY::No);
         }
         builder.args(&[name, "."]);
         println!("preopen: {:?}", workspace);
@@ -79,6 +81,7 @@ fn run(name: &str, inherit_stdio: bool) -> Result<()> {
         let mut store = Store::new(&ENGINE, ctx);
         let (command, _instance) = Command::instantiate(&mut store, &get_component(name), &linker)?;
         command
+            .wasi_cli_run()
             .call_run(&mut store)?
             .map_err(|()| anyhow::anyhow!("run returned a failure"))?;
         Ok(())
@@ -178,8 +181,8 @@ fn interesting_paths() {
     run("interesting_paths", false).unwrap()
 }
 #[test_log::test]
-fn isatty() {
-    run("isatty", false).unwrap()
+fn regular_file_isatty() {
+    run("regular_file_isatty", false).unwrap()
 }
 #[test_log::test]
 fn nofollow_errors() {
@@ -273,6 +276,19 @@ fn stdio() {
     run("stdio", false).unwrap()
 }
 #[test_log::test]
+fn stdio_isatty() {
+    // If the test process is setup such that stdio is a terminal:
+    if test_programs::stdio_is_terminal() {
+        // Inherit stdio, test asserts each is a tty:
+        run("stdio_isatty", true).unwrap()
+    }
+}
+#[test_log::test]
+fn stdio_not_isatty() {
+    // Don't inherit stdio, test asserts not isatty:
+    run("stdio_not_isatty", false).unwrap()
+}
+#[test_log::test]
 fn symlink_create() {
     run("symlink_create", false).unwrap()
 }
diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs
index c74cae900..77a3e71ff 100644
--- a/crates/test-programs/tests/wasi-preview2-components.rs
+++ b/crates/test-programs/tests/wasi-preview2-components.rs
@@ -5,7 +5,7 @@ use wasmtime::{component::Linker, Config, Engine, Store};
 use wasmtime_wasi::preview2::{
     command::{add_to_linker, Command},
     pipe::MemoryOutputPipe,
-    DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView,
+    DirPerms, FilePerms, IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView,
 };
 
 lazy_static::lazy_static! {
@@ -43,7 +43,9 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> {
         if inherit_stdio {
             builder.inherit_stdio();
         } else {
-            builder.stdout(stdout.clone()).stderr(stderr.clone());
+            builder
+                .stdout(stdout.clone(), IsATTY::No)
+                .stderr(stderr.clone(), IsATTY::No);
         }
         builder.args(&[name, "."]);
         println!("preopen: {:?}", workspace);
@@ -80,6 +82,7 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> {
         let (command, _instance) =
             Command::instantiate_async(&mut store, &get_component(name), &linker).await?;
         command
+            .wasi_cli_run()
             .call_run(&mut store)
             .await?
             .map_err(|()| anyhow::anyhow!("run returned a failure"))?;
@@ -180,8 +183,8 @@ async fn interesting_paths() {
     run("interesting_paths", false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
-async fn isatty() {
-    run("isatty", false).await.unwrap()
+async fn regular_file_isatty() {
+    run("regular_file_isatty", false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn nofollow_errors() {
@@ -281,6 +284,19 @@ async fn stdio() {
     run("stdio", false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn stdio_isatty() {
+    // If the test process is setup such that stdio is a terminal:
+    if test_programs::stdio_is_terminal() {
+        // Inherit stdio, test asserts each is not tty:
+        run("stdio_isatty", true).await.unwrap()
+    }
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn stdio_not_isatty() {
+    // Don't inherit stdio, test asserts each is not tty:
+    run("stdio_not_isatty", false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn symlink_create() {
     run("symlink_create", false).await.unwrap()
 }
diff --git a/crates/test-programs/tests/wasi-tokio.rs b/crates/test-programs/tests/wasi-tokio.rs
index bd673704c..9f8390c91 100644
--- a/crates/test-programs/tests/wasi-tokio.rs
+++ b/crates/test-programs/tests/wasi-tokio.rs
@@ -162,8 +162,8 @@ async fn interesting_paths() {
     run("interesting_paths", true).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
-async fn isatty() {
-    run("isatty", true).await.unwrap()
+async fn regular_file_isatty() {
+    run("regular_file_isatty", false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn nofollow_errors() {
@@ -260,6 +260,19 @@ async fn stdio() {
     run("stdio", true).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn stdio_isatty() {
+    // Only a valid test if the host executable's stdio is a terminal:
+    if test_programs::stdio_is_terminal() {
+        // Inherit stdio, test asserts it is a tty:
+        run("stdio_isatty", true).await.unwrap()
+    }
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn stdio_not_isatty() {
+    // Don't inherit stdio, test asserts each is not tty:
+    run("stdio_not_isatty", false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn symlink_create() {
     run("symlink_create", true).await.unwrap()
 }
diff --git a/crates/test-programs/wasi-tests/src/bin/isatty.rs b/crates/test-programs/wasi-tests/src/bin/regular_file_isatty.rs
similarity index 92%
rename from crates/test-programs/wasi-tests/src/bin/isatty.rs
rename to crates/test-programs/wasi-tests/src/bin/regular_file_isatty.rs
index c8bb3a397..6a8c821be 100644
--- a/crates/test-programs/wasi-tests/src/bin/isatty.rs
+++ b/crates/test-programs/wasi-tests/src/bin/regular_file_isatty.rs
@@ -1,7 +1,7 @@
 use std::{env, process};
 use wasi_tests::open_scratch_directory;
 
-unsafe fn test_isatty(dir_fd: wasi::Fd) {
+unsafe fn test_file_isatty(dir_fd: wasi::Fd) {
     // Create a file in the scratch directory and test if it's a tty.
     let file_fd =
         wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_CREAT, 0, 0, 0).expect("opening a file");
@@ -38,5 +38,5 @@ fn main() {
     };
 
     // Run the tests.
-    unsafe { test_isatty(dir_fd) }
+    unsafe { test_file_isatty(dir_fd) }
 }
diff --git a/crates/test-programs/wasi-tests/src/bin/stdio_isatty.rs b/crates/test-programs/wasi-tests/src/bin/stdio_isatty.rs
new file mode 100644
index 000000000..7b0f37708
--- /dev/null
+++ b/crates/test-programs/wasi-tests/src/bin/stdio_isatty.rs
@@ -0,0 +1,22 @@
+unsafe fn test_stdio_isatty() {
+    assert_eq!(
+        libc::isatty(libc::STDIN_FILENO as std::os::raw::c_int),
+        1,
+        "stdin is a tty"
+    );
+    assert_eq!(
+        libc::isatty(libc::STDOUT_FILENO as std::os::raw::c_int),
+        1,
+        "stdout is a tty"
+    );
+    assert_eq!(
+        libc::isatty(libc::STDERR_FILENO as std::os::raw::c_int),
+        1,
+        "stderr is a tty"
+    );
+}
+
+fn main() {
+    // Run the tests.
+    unsafe { test_stdio_isatty() }
+}
diff --git a/crates/test-programs/wasi-tests/src/bin/stdio_not_isatty.rs b/crates/test-programs/wasi-tests/src/bin/stdio_not_isatty.rs
new file mode 100644
index 000000000..c67d3cf3b
--- /dev/null
+++ b/crates/test-programs/wasi-tests/src/bin/stdio_not_isatty.rs
@@ -0,0 +1,22 @@
+unsafe fn test_stdio_not_isatty() {
+    assert_eq!(
+        libc::isatty(libc::STDIN_FILENO as std::os::raw::c_int),
+        0,
+        "stdin is not a tty"
+    );
+    assert_eq!(
+        libc::isatty(libc::STDOUT_FILENO as std::os::raw::c_int),
+        0,
+        "stdout is not a tty"
+    );
+    assert_eq!(
+        libc::isatty(libc::STDERR_FILENO as std::os::raw::c_int),
+        0,
+        "stderr is not a tty"
+    );
+}
+
+fn main() {
+    // Run the tests.
+    unsafe { test_stdio_not_isatty() }
+}
diff --git a/crates/wasi/wit/test.wit b/crates/wasi/wit/test.wit
index 09498f62c..447304cba 100644
--- a/crates/wasi/wit/test.wit
+++ b/crates/wasi/wit/test.wit
@@ -1,11 +1,11 @@
 // only used as part of `test-programs`
 world test-reactor {
 
-  import wasi:cli-base/environment
+  import wasi:cli/environment
   import wasi:io/streams
   import wasi:filesystem/types
   import wasi:filesystem/preopens
-  import wasi:cli-base/exit
+  import wasi:cli/exit
 
   export add-strings: func(s: list<string>) -> u32
   export get-strings: func() -> list<string>
@@ -21,8 +21,8 @@ world test-reactor {
 world test-command {
   import wasi:poll/poll
   import wasi:io/streams
-  import wasi:cli-base/environment
-  import wasi:cli-base/stdin
-  import wasi:cli-base/stdout
-  import wasi:cli-base/stderr
+  import wasi:cli/environment
+  import wasi:cli/stdin
+  import wasi:cli/stdout
+  import wasi:cli/stderr
 }