Repository: bytecodealliance/wasmtime
Pull request #6806 (2023-08-04 23:50:56)

Sync wasi-cli with wit definitions in standards repo 
This PR synchronizes the wasi:cli package with the upstream spec definition.

As part of this PR I upstreamed changes from the wasmtime repo:
https://github.com/WebAssembly/wasi-cli/pull/19
https://github.com/WebAssembly/wasi-cli/pull/20

The changes arriving downstream in this change are
1. eliminating the patchwork of "temporary" fixes to the cli definition as the wit language evolved alongside the implementation. This involved calling the package "cli-base" in the wasmtime repo, and putting world definitions separately in a "preview" package.
2. adding implementations of the `terminal-{input, output, stdin, stdout, stderr}` functions to the cli implementation, which allows us to implement `isatty`, bringing preview2's view of stdio to parity with preview 1, where fd_filestat_get could be used to determine whether stdio were ttys. I made some changes in wasi-tests to exercise this functionality to show that it works in all 5 of our WASI implementations. (yes i roll my eyes every time i type all 5 wasi implementations too. because i wrote them. what have i done. what did they do to us)


elliottt: (I rebased the branch on main to resolve the merge conflict.)

Patch:
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/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
 }