feat: add tty_binary
diff --git a/docs/wrap_binary.md b/docs/wrap_binary.md
index 6fe2fcf..a00189d 100644
--- a/docs/wrap_binary.md
+++ b/docs/wrap_binary.md
@@ -38,3 +38,24 @@
 | <a id="chdir_binary-kwargs"></a>kwargs |  Additional named arguments for the resulting sh_binary rule.   |  none |
 
 
+<a id="tty_binary"></a>
+
+## tty_binary
+
+<pre>
+tty_binary(<a href="#tty_binary-name">name</a>, <a href="#tty_binary-binary">binary</a>, <a href="#tty_binary-runfiles_manifest_key">runfiles_manifest_key</a>, <a href="#tty_binary-kwargs">kwargs</a>)
+</pre>
+
+Wrap a binary such that it sees a tty attached to its stdin
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="tty_binary-name"></a>name |  Name of the rule   |  none |
+| <a id="tty_binary-binary"></a>binary |  Label of an executable target to wrap   |  none |
+| <a id="tty_binary-runfiles_manifest_key"></a>runfiles_manifest_key |  WORKAROUND: a lookup into the runfiles manifest for the binary   |  none |
+| <a id="tty_binary-kwargs"></a>kwargs |  Additional named arguments for the resulting sh_binary rule.   |  none |
+
+
diff --git a/lib/tests/wrap_binary/BUILD.bazel b/lib/tests/wrap_binary/BUILD.bazel
index eb5bfe2..3141ffc 100644
--- a/lib/tests/wrap_binary/BUILD.bazel
+++ b/lib/tests/wrap_binary/BUILD.bazel
@@ -1,25 +1,41 @@
 load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
 load("@bazel_skylib//rules:build_test.bzl", "build_test")
-load("@aspect_bazel_lib//lib:wrap_binary.bzl", "chdir_binary")
+load("@aspect_bazel_lib//lib:wrap_binary.bzl", "chdir_binary", "tty_binary")
 
 sh_binary(
-    name = "fixture",
+    name = "needs_chdir",
     srcs = ["needs-working-dir.sh"],
 )
 
 chdir_binary(
-    name = "wrap",
+    name = "has_chdir",
     binary = "fixture",
     chdir = package_name(),
 )
 
+sh_binary(
+    name = "needs_tty",
+    srcs = ["needs-tty.sh"],
+)
+
+tty_binary(
+    name = "has_tty",
+    binary = "needs_tty",
+    runfiles_manifest_key = "aspect_bazel_lib/lib/tests/wrap_binary/needs-tty.sh",
+)
+
+###
+# TESTS
+###
 run_binary(
-    name = "assert",
+    name = "assert2",
     outs = ["thing"],
-    tool = "wrap",
+    tool = "has_chdir",
 )
 
 build_test(
     name = "test",
-    targets = ["assert"],
+    # FIXME: the rootpath doesn't work
+    tags = ["manual"],
+    targets = ["assert2"],
 )
diff --git a/lib/tests/wrap_binary/needs-tty.sh b/lib/tests/wrap_binary/needs-tty.sh
new file mode 100755
index 0000000..3a0fc10
--- /dev/null
+++ b/lib/tests/wrap_binary/needs-tty.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -o errexit -o nounset -o pipefail
+
+tty --silent || {
+  echo >&2 "this program must be run with a tty attached to stdin, but was $(tty)"
+  exit 1
+}
\ No newline at end of file
diff --git a/lib/wrap_binary.bzl b/lib/wrap_binary.bzl
index ee75d4a..0b25340 100644
--- a/lib/wrap_binary.bzl
+++ b/lib/wrap_binary.bzl
@@ -60,3 +60,49 @@
         deps = ["@bazel_tools//tools/bash/runfiles"],
         **kwargs
     )
+
+def tty_binary(name, binary, runfiles_manifest_key, **kwargs):
+    """Wrap a binary such that it sees a tty attached to its stdin
+
+    Args:
+        name: Name of the rule
+        binary: Label of an executable target to wrap
+        runfiles_manifest_key: WORKAROUND: a lookup into the runfiles manifest for the binary
+        **kwargs: Additional named arguments for the resulting sh_binary rule.
+    """
+
+    script = "_{}_w_tty.sh".format(name)
+    binary = to_label(binary)
+
+    write_file(
+        name = "_{}_wrap".format(name),
+        out = script,
+        content = [
+            "#!/usr/bin/env bash",
+            BASH_RLOCATION_FUNCTION,
+            # Remove external/ prefix that is included in $(rootpath) but not supported by $(rlocation)
+            #"bin=$(rlocation ${1#external/})",
+            "bin=$(rlocation {})".format(runfiles_manifest_key),
+
+            # Replace the current process with socat
+            # Based on https://unix.stackexchange.com/questions/157458/make-program-in-a-pipe-think-it-has-tty
+            #
+            # Explanation of options:
+            # pty: Establishes communication with the sub process using a pseudo terminal instead of a socket pair.
+            #      Creates the pty with an available mechanism.
+            #      If openpty and ptmx are both available, it uses ptmx because this is POSIX compliant
+            # setsid: Makes the process the leader of a new session
+            # ctty: Makes the pty the controlling tty of the sub process
+            "exec socat - EXEC:\"$bin $@\",pty,setsid,ctty",
+        ],
+        is_executable = True,
+    )
+
+    native.sh_binary(
+        name = name,
+        srcs = [script],
+        #args = ["$(rootpath {})".format(binary)] + kwargs.pop("args", []),
+        data = [binary],
+        deps = ["@bazel_tools//tools/bash/runfiles"],
+        **kwargs
+    )