pty_compat 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ce2fec7856c5d46fb0b88e0f0ecc86828593ae8eb0ee1559c7778b35607902a1
4
+ data.tar.gz: 5dcaaf25e175c108f3fb04cf74a8b4ae0c03b898afea3dbb0b40be14547d4edc
5
+ SHA512:
6
+ metadata.gz: f3411c48494e354226b1d8178e845d4912648648a03d5a54017583bbc9fb0adbbbf4c90d52d7a4ed70aa1b692e158621f0ac1529bd2a320e6e42dc870a4183a1
7
+ data.tar.gz: 1e9e6956e34c4cdba26db42f3e9e2d1499829a6fe3d3298c2e7cae3763915dd4c4667889b2f3e03c61abeacd1af25e37997a8f056a1143dd6a59691bf2d27034
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # [v0.0.1](https://github.com/Muriel-Salvan/pty_compat/compare/...v0.0.1) (2026-06-29 12:55:30)
2
+
3
+ ### Patches
4
+
5
+ * [chore: lower minimum coverage threshold to 90%](https://github.com/Muriel-Salvan/pty_compat/commit/0adc20238ded82c8511a65f0e89ecab8482900e9)
6
+ * [ci: add node-pty installation step in CI workflow](https://github.com/Muriel-Salvan/pty_compat/commit/c4e77e60e4a3be59f4034dbcf2a6a3fbb3ed6d4f)
7
+ * [docs: update README with comprehensive documentation and Windows support](https://github.com/Muriel-Salvan/pty_compat/commit/b62cab654dd16a30233f89e75290f2a5b3f07d2b)
8
+ * [feat: add SimpleCov for code coverage with 95% minimum threshold](https://github.com/Muriel-Salvan/pty_compat/commit/bfe51ddad41652f8501da02c075bd8f5a7f54bc6)
9
+ * [docs: add YARD documentation infrastructure and enforce 100% documentation coverage](https://github.com/Muriel-Salvan/pty_compat/commit/586db5fdc71af12e0a4cbe9205ec2717476c790b)
10
+ * [fix: set UTF-8 encoding on I/O streams and fix PTY.last_status + Added unit tests](https://github.com/Muriel-Salvan/pty_compat/commit/f8863af7a66660a01684f5e0f960208faed17503)
11
+ * [feat: support environment variables and lazy process status in spawn](https://github.com/Muriel-Salvan/pty_compat/commit/1db60e395f8e915a40817ec8f8a8706fdcee168e)
12
+ * [feat: expose last process status via `last_status` method](https://github.com/Muriel-Salvan/pty_compat/commit/1f3a56c3bcffd953e164ad73bfa9900c43f531f9)
13
+ * [fix: resolve node-pty module path relative to project directory](https://github.com/Muriel-Salvan/pty_compat/commit/8d68466b5dc35ca671fb8458485b02066684a645)
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ <div align="center">
2
+
3
+ # pty_compat
4
+
5
+ Make Ruby's `PTY` work on all platforms, including Windows
6
+
7
+ [![License](https://img.shields.io/github/license/Muriel-Salvan/pty_compat?style=for-the-badge)](https://github.com/Muriel-Salvan/pty_compat/blob/main/LICENSE)
8
+ [![Gem Version](https://img.shields.io/gem/v/pty_compat?style=for-the-badge)](https://rubygems.org/gems/pty_compat)
9
+ [![CI](https://img.shields.io/github/actions/workflow/status/Muriel-Salvan/pty_compat/continuous_integration.yml?style=for-the-badge)](https://github.com/Muriel-Salvan/pty_compat/actions)
10
+ [![Stars](https://img.shields.io/github/stars/Muriel-Salvan/pty_compat?style=for-the-badge)](https://github.com/Muriel-Salvan/pty_compat/stargazers)
11
+
12
+ </div>
13
+
14
+ ## What is this?
15
+
16
+ Ruby's built-in `PTY` module is not available on Windows. `pty_compat` transparently replaces it with an equivalent implementation using [`node-pty`](https://github.com/microsoft/node-pty), so your code works everywhere without changes.
17
+
18
+ A single `require 'pty_compat'` patches `PTY.spawn` to fall back to a Node.js bridge when the native module is unavailable. No migration, no conditional logic, no platform checks.
19
+
20
+ ## Table of contents
21
+
22
+ - [What is this?](#what-is-this)
23
+ - [Quick Start](#quick-start)
24
+ - [Installation](#installation)
25
+ - [Usage](#usage)
26
+ - [Requirements](#requirements)
27
+ - [Features](#features)
28
+ - [Public API](#public-api)
29
+ - [Documentation](#documentation)
30
+ - [How it works](#how-it-works)
31
+ - [Development](#development)
32
+ - [Contributing](#contributing)
33
+ - [License](#license)
34
+
35
+ ## Quick Start
36
+
37
+ ### Installation
38
+
39
+ ```sh
40
+ bundle add pty_compat
41
+ ```
42
+
43
+ If you're not using Bundler:
44
+
45
+ ```sh
46
+ gem install pty_compat
47
+ ```
48
+
49
+ On Windows, you also need `node-pty`:
50
+
51
+ ```sh
52
+ npm install node-pty
53
+ ```
54
+
55
+ > [!TIP]
56
+ > If your project does not use Node.js, you can install `node-pty` locally. The bridge script resolves it from the current working directory.
57
+
58
+ ### Usage
59
+
60
+ ```ruby
61
+ require 'pty_compat'
62
+
63
+ # Non-block form
64
+ reader, writer, pid = PTY.spawn('ping', '-c', '3', 'example.com')
65
+ writer.puts('input')
66
+ reader.each_line { |line| puts line }
67
+ Process.wait(pid)
68
+
69
+ # Block form
70
+ PTY.spawn('ping', '-c', '3', 'example.com') do |reader, writer, pid|
71
+ writer.puts('input')
72
+ reader.each_line { |line| puts line }
73
+ end
74
+
75
+ # Retrieve the exit status portably
76
+ status = PTY.last_status
77
+ ```
78
+
79
+ ## Requirements
80
+
81
+ - Ruby >= 3.1
82
+ - Node.js and `node-pty` (only required on platforms without native `PTY` support, typically Windows)
83
+
84
+ ## Features
85
+
86
+ - **Zero-config drop-in.** A single `require` replaces `PTY.spawn` on Windows — no configuration, no platform checks, no conditional logic.
87
+ - **Portable exit status.** Use `PTY.last_status` to retrieve the exit code on any platform instead of relying on `$?`.
88
+ - **Non-block & block forms.** Supports both `PTY.spawn(command, args...) -> [reader, writer, pid]` and `PTY.spawn(command, args...) { |reader, writer, pid| ... }` forms.
89
+ - **Windows support.** Leverages Microsoft's [`node-pty`](https://github.com/microsoft/node-pty) to provide a proper PTY on Windows, where Ruby's native `PTY` is unavailable.
90
+ - **Lightweight.** The Ruby codebase is minimal, delegating the heavy lifting to a well-maintained native module.
91
+ - **Works on all platforms.** Falls back to the `node-pty` bridge only when the native `PTY` module is unavailable; otherwise uses the standard library unchanged.
92
+
93
+ ## Public API
94
+
95
+ ### `PTY.spawn(command, *args) -> [reader, writer, pid]`
96
+
97
+ Spawns a new process attached to a pseudo-terminal.
98
+
99
+ | Parameter | Type | Description |
100
+ |-----------|------|-------------|
101
+ | `command` | `String` | The command to execute (e.g. `'ping'`). |
102
+ | `*args` | `String...` | Zero or more arguments passed to the command. |
103
+
104
+ **Non-block form** returns a three-element array:
105
+
106
+ | Element | Type | Description |
107
+ |---------|------|-------------|
108
+ | `reader` | `IO` | Readable IO (stdout + stderr merged). |
109
+ | `writer` | `IO` | Writable IO (stdin of the spawned process). |
110
+ | `pid` | `Integer` | Process ID of the spawned process. |
111
+
112
+ ### `PTY.spawn(command, *args) { |reader, writer, pid| block }`
113
+
114
+ **Block form** yields `reader`, `writer`, and `pid` to the given block, and automatically closes the IOs after the block returns.
115
+
116
+ ### `PTY.last_status -> Process::Status | nil`
117
+
118
+ Returns the exit status of the last spawned process.
119
+
120
+ - On platforms with native `PTY`, mirrors `$?`.
121
+ - On the fallback path, returns a `Process::Status` constructed from the exit code captured by the `node-pty` bridge.
122
+ - Returns `nil` if no process has been spawned yet or if the last spawn failed.
123
+
124
+ > [!TIP]
125
+ > Prefer `PTY.last_status` over `$?` for portable code that runs on both Windows and Unix.
126
+
127
+ ## Documentation
128
+
129
+ - [RubyDoc](https://www.rubydoc.info/gems/pty_compat)
130
+
131
+ ## How it works
132
+
133
+ 1. `pty_compat` tries to load Ruby's standard `PTY` library first.
134
+ 2. On `LoadError` (as raised on Windows), it prepends `PtyCompat::NodePty` into `PTY`.
135
+ 3. `PtyCompat::NodePty` implements `PTY.spawn` through a Node.js bridge that uses `node-pty` to create a pseudo-terminal.
136
+ 4. Stdout and stderr are merged into a single readable IO, matching the behaviour of Ruby's native `PTY.spawn`.
137
+
138
+ ```
139
+ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
140
+ │ Ruby code │────▶│ PTY.spawn(...) │────▶│ node-pty │
141
+ │ (your app) │ │ (patched) │ │ bridge.js │
142
+ └──────────────┘ └──────────────────┘ └──────┬───────┘
143
+
144
+
145
+ ┌──────────────┐
146
+ │ Command │
147
+ │ (shell, │
148
+ │ process) │
149
+ └──────────────┘
150
+ ```
151
+
152
+ ### `PTY.last_status`
153
+
154
+ On platforms with native `PTY`, `PTY.last_status` returns `$?` (`Process::Status`). On the fallback path, the bridge captures the exit code and exposes it through the same method. Prefer this over `$?` for portable code.
155
+
156
+ ### Why not a pure Ruby PTY?
157
+
158
+ Alternative approaches rely on platform-specific C extensions that are painful to compile on Windows, or expose an incomplete `PTY` interface. `pty_compat` delegates the heavy lifting to [`node-pty`](https://github.com/microsoft/node-pty), a well-maintained native module by Microsoft that supports Windows, macOS, and Linux. This keeps the Ruby code small and the platform coverage broad.
159
+
160
+ ## Development
161
+
162
+ ```sh
163
+ bundle install
164
+ ```
165
+
166
+ Run the tests:
167
+
168
+ ```sh
169
+ bundle exec rspec
170
+ ```
171
+
172
+ Lint with RuboCop:
173
+
174
+ ```sh
175
+ bundle exec rubocop
176
+ ```
177
+
178
+ ## Contributing
179
+
180
+ Bug reports and pull requests are welcome on GitHub at [Muriel-Salvan/pty_compat](https://github.com/Muriel-Salvan/pty_compat).
181
+
182
+ <a href="https://github.com/Muriel-Salvan/pty_compat/graphs/contributors">
183
+ <img src="https://contrib.rocks/image?repo=Muriel-Salvan/pty_compat" />
184
+ </a>
185
+
186
+ ## License
187
+
188
+ The gem is available as open source under the terms of the [BSD-3-Clause License](LICENSE).
189
+
190
+ ---
191
+
192
+ <div align="center">
193
+
194
+ [![Star History Chart](https://api.star-history.com/svg?repos=Muriel-Salvan/pty_compat&type=Date)](https://star-history.com/#Muriel-Salvan/pty_compat&Date)
195
+
196
+ </div>
data/TODO.md ADDED
File without changes
@@ -0,0 +1,38 @@
1
+ // This NodeJS bridge is used only on platforms that don't support Ruby's PTY standard library.
2
+ // This is needed to use the CLIs from Ruby on Windows.
3
+ // This bridge will use the node-pty module of NodeJS to bridge a command line through a PTY interface.
4
+ // To use it the user must first install the node-pty module in the project that will be using the pty_compat Rubygem, like this:
5
+ // npm install node-pty
6
+
7
+ const pty = require(
8
+ require.resolve("node-pty", {
9
+ paths: [process.cwd()]
10
+ })
11
+ );
12
+
13
+ const [cmd, ...args] = process.argv.slice(2);
14
+
15
+ const shell = pty.spawn(cmd, args, {
16
+ name: "xterm-color",
17
+ cols: 80,
18
+ rows: 30,
19
+ cwd: process.cwd(),
20
+ env: process.env
21
+ });
22
+
23
+ // Send output to Ruby
24
+ shell.on("data", (data) => {
25
+ process.stdout.write(data);
26
+ });
27
+
28
+ // Read commands from Ruby (stdin)
29
+ process.stdin.setEncoding("utf8");
30
+
31
+ process.stdin.on("data", (data) => {
32
+ shell.write(data);
33
+ });
34
+
35
+ // Catch the exit properly
36
+ shell.on("exit", (code, signal) => {
37
+ process.exit(code ?? 0);
38
+ });
@@ -0,0 +1,95 @@
1
+ require 'open3'
2
+
3
+ module PtyCompat
4
+ # Provide a similar interface as the PTY one that could work on platforms that don't support PTY (for example Windows).
5
+ # Internally uses NodeJS's node-pty.
6
+ module NodePty
7
+ # @!group Public API
8
+
9
+ # Spawn a command in a PTY and return or yield its outputs, input and pid
10
+ #
11
+ # @param args [Array] The command arguments to execute (see [`::PTY#spawn`](https://www.rubydoc.info/stdlib/pty/PTY%2Espawn))
12
+ # @yield An optional code called with all PTY outputs, input and PID.
13
+ # @yieldparam r [IO] The reader output (containing stdout and stderr).
14
+ # @yieldparam w [IO] The writer input (containing stdin).
15
+ # @yieldparam pid [Integer] The process PID.
16
+ # @return [Array<IO, Integer>, nil] The reader, writer and PID of the process, or nil if used with a yielded block.
17
+ # - r [IO] The reader output (containing stdout and stderr).
18
+ # - w [IO] The writer input (containing stdin).
19
+ # - pid [Integer] The process PID.
20
+ def spawn(*args)
21
+ env, cmd = args.first.is_a?(Hash) ? [[args.first], args[1..]] : [[], args]
22
+ node_args = env + ['node', "#{__dir__}/assets/node_pty_bridge.js"] + cmd
23
+ if block_given?
24
+ Open3.popen3(*node_args) do |stdin, stdout, stderr, wait_thr|
25
+ @last_wait_thr = wait_thr
26
+ yield popen3_to_pty(stdin, stdout, stderr, wait_thr)
27
+ end
28
+ else
29
+ stdin, stdout, stderr, wait_thr = Open3.popen3(*node_args)
30
+ @last_wait_thr = wait_thr
31
+ popen3_to_pty(stdin, stdout, stderr, wait_thr)
32
+ end
33
+ end
34
+
35
+ # @return [Process::Status] Last process status
36
+ def last_status
37
+ @last_wait_thr.value
38
+ end
39
+
40
+ private
41
+
42
+ # Convert the popen3 descriptors to PTY ones
43
+ #
44
+ # @param stdin [IO] The stdin descriptor
45
+ # @param stdout [IO] The stdout descriptor
46
+ # @param stderr [IO] The stderr descriptor
47
+ # @param wait_thread [Process::Waiter] The process information
48
+ # @return [Array<IO, Integer>] The corresponding PTY reader, writer and PID.
49
+ # - r [IO] The reader output (containing stdout and stderr).
50
+ # - w [IO] The writer input (containing stdin).
51
+ # - pid [Integer] The process PID.
52
+ def popen3_to_pty(stdin, stdout, stderr, wait_thread)
53
+ stdout.set_encoding(Encoding::UTF_8)
54
+ stderr.set_encoding(Encoding::UTF_8)
55
+ stdin.set_encoding(Encoding::UTF_8)
56
+ # Create a pipe to combine stdout and stderr into a single IO
57
+ combined_r, combined_w = IO.pipe
58
+ # Reader thread for stdout
59
+ stdout_reader = Thread.new do
60
+ IO.copy_stream(stdout, combined_w)
61
+ rescue IOError
62
+ # Ignore errors from closed pipe
63
+ ensure
64
+ begin
65
+ stdout.close
66
+ rescue RuntimeError
67
+ nil
68
+ end
69
+ end
70
+ # Reader thread for stderr
71
+ stderr_reader = Thread.new do
72
+ IO.copy_stream(stderr, combined_w)
73
+ rescue IOError
74
+ # Ignore errors from closed pipe
75
+ ensure
76
+ begin
77
+ stderr.close
78
+ rescue RuntimeError
79
+ nil
80
+ end
81
+ end
82
+ # Closer thread: close the write end of the combined pipe once both stdout and stderr have finished being read.
83
+ Thread.new do
84
+ stdout_reader.join
85
+ stderr_reader.join
86
+ begin
87
+ combined_w.close
88
+ rescue RuntimeError
89
+ nil
90
+ end
91
+ end
92
+ [combined_r, stdin, wait_thread.pid]
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,26 @@
1
+ require 'English'
2
+
3
+ # Augment PTY module with last_status
4
+ module PTY
5
+ # @!group Public API
6
+
7
+ # @return [Process::Status] Last process status.
8
+ # Use this instead of $? as some workaround methods don't set $? properly and we can't modify this variable.
9
+ def self.last_status
10
+ # Default implementation
11
+ $CHILD_STATUS
12
+ end
13
+ end
14
+
15
+ begin
16
+ require 'pty'
17
+ rescue LoadError => e
18
+ if e.message == 'cannot load such file -- pty'
19
+ module PTY
20
+ class << self
21
+ # Fallback on node-pty
22
+ prepend PtyCompat::NodePty
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ module PtyCompat
2
+ # @!group Public API
3
+
4
+ # Gem version
5
+ VERSION = '1.0.0'
6
+ end
data/lib/pty_compat.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'zeitwerk'
2
+
3
+ Zeitwerk::Loader.for_gem.setup
4
+
5
+ # Provide ways to implement Ruby's PTY's interface on all platofrms.
6
+ module PtyCompat
7
+ end
8
+
9
+ # Make sure PTY is loaded with eventual fallbacks.
10
+ require 'pty_compat/patches/pty'
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pty_compat
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Muriel Salvan
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zeitwerk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.7'
26
+ email: muriel@x-aeon.com
27
+ executables: []
28
+ extensions: []
29
+ extra_rdoc_files: []
30
+ files:
31
+ - CHANGELOG.md
32
+ - README.md
33
+ - TODO.md
34
+ - lib/pty_compat.rb
35
+ - lib/pty_compat/assets/node_pty_bridge.js
36
+ - lib/pty_compat/node_pty.rb
37
+ - lib/pty_compat/patches/pty.rb
38
+ - lib/pty_compat/version.rb
39
+ homepage: https://github.com/Muriel-Salvan/pty_compat
40
+ licenses:
41
+ - BSD-3-Clause
42
+ metadata:
43
+ rubygems_mfa_required: 'true'
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.1'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.6.9
59
+ specification_version: 4
60
+ summary: Make Ruby's PTY work on all platforms
61
+ test_files: []