ripgrep_wasm 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48e7d7bd34377c527f6b51b649ac6e997f058d0a2d8135c590650fb16c986489
4
- data.tar.gz: dd19a052210eba94d85fbe5e46d703a6ce6d8a339e59256410fc5cce389bb656
3
+ metadata.gz: 4b2cecf649bbb67e17109c6a368898b8aa036800961105dbd558191dd4ec5572
4
+ data.tar.gz: 3565f9f98ac34d4c5fa79a0442166a09a6b3384b83f1e78410bd68abf94667c1
5
5
  SHA512:
6
- metadata.gz: 3c5dcb66c0aa34f49d0eb379e92984ef20e99f3c00220eb493cd40aed75cd58c4eeebdd0e0555c18ea65aa2c5a3699b2568076375f7c626245eb0a913de0cf7a
7
- data.tar.gz: 81934964911d5905b6cf5ca727de8902a440e7af00e0d447d18d8121771b28d7523e55bb2f56e267b609e5a0a3964d0164b41ad605e105292d47fa3fd8fcecb6
6
+ metadata.gz: 51dc1c74814dc460ebecc8e00b539dc41a374b88eb576dc439a15fd7f33ceb92412c8237e0c5caf6d286eb4dfbc5443b4b17111011fb2f5c732656a060cce31e
7
+ data.tar.gz: d4b4f371a071cc039449270ac1b481422b4d3aeea0bcb2b61c87b3ad7af3412c2fbca9f9d2e6619d22a582af29147ec8bbc3d45196c6fd8e0a34354ab6c71b5e
data/AGENTS.md ADDED
@@ -0,0 +1,395 @@
1
+ # AGENTS.md — Guidelines for WASM Binary Wrapper Ruby Gems
2
+
3
+ This document describes the architecture and conventions for Ruby gems that wrap a
4
+ WebAssembly binary (executed via a WASI runtime like wasmtime). It is meant to be
5
+ copied and adapted for each new gem of this kind.
6
+
7
+ ---
8
+
9
+ ## 1. Naming Conventions
10
+
11
+ | Concept | Convention | Example |
12
+ |---------|-----------|---------|
13
+ | Gem name | `<tool>_wasm` (snake_case) | `pandoc_wasm` |
14
+ | Module name | `<Tool>Wasm` (PascalCase) | `PandocWasm` |
15
+ | Binary asset | `<tool>.wasm` | `pandoc.wasm` |
16
+ | GitHub repo | `<owner>/<tool>-wasm` | `NathanHimpens/pandoc-wasm` |
17
+
18
+ ---
19
+
20
+ ## 2. File Layout
21
+
22
+ ```
23
+ lib/
24
+ <tool>_wasm.rb # Main entry point — module configuration + public API
25
+ <tool>_wasm/
26
+ version.rb # VERSION constant
27
+ downloader.rb # Downloads the .wasm binary from GitHub Releases
28
+ runner.rb # Wraps the WASI runtime system call
29
+ test/
30
+ test_helper.rb # Minitest bootstrap + module state reset helper
31
+ <tool>_wasm_test.rb # Tests for main module (config, delegation, introspection)
32
+ runner_test.rb # Tests for Runner (command building, errors, result)
33
+ downloader_test.rb # Tests for Downloader (signature, errors, path expansion)
34
+ integration_test.rb # Full user workflow scenario (configure, download, run)
35
+ <tool>_wasm.gemspec # Gem specification
36
+ Rakefile # rake test runs test/**/*_test.rb via Minitest
37
+ AGENTS.md # This file — reusable guidelines
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 3. Public API Contract
43
+
44
+ Every gem MUST expose exactly these public methods on the top-level module:
45
+
46
+ ### Configuration
47
+
48
+ ```ruby
49
+ # Get / set the absolute path where the .wasm binary lives.
50
+ # Defaults to lib/<tool>_wasm/<tool>.wasm inside the installed gem.
51
+ <Tool>Wasm.binary_path # => String
52
+ <Tool>Wasm.binary_path = "/path" # setter
53
+
54
+ # Get / set the WASI runtime executable name.
55
+ # Defaults to "wasmtime".
56
+ <Tool>Wasm.runtime # => String
57
+ <Tool>Wasm.runtime = "wasmer" # setter
58
+ ```
59
+
60
+ ### Download
61
+
62
+ ```ruby
63
+ # Download the .wasm binary from the latest GitHub Release to `binary_path`.
64
+ # Creates intermediate directories if needed.
65
+ # Returns true on success, raises on failure.
66
+ <Tool>Wasm.download_to_binary_path!
67
+ ```
68
+
69
+ ### Execution
70
+
71
+ ```ruby
72
+ # Run the wasm binary. All positional arguments are passed through to the binary.
73
+ # Translates to:
74
+ # <runtime> run --dir <wasm_dir> <binary_path> <args...>
75
+ #
76
+ # Returns a Hash: { stdout: String, stderr: String, success: Boolean }
77
+ # Raises BinaryNotFound if the binary is missing at binary_path.
78
+ # Raises ExecutionError (with stderr) on non-zero exit code.
79
+ <Tool>Wasm.run(*args, wasm_dir: ".")
80
+ ```
81
+
82
+ ### Introspection
83
+
84
+ ```ruby
85
+ # Returns true if the binary exists at binary_path.
86
+ <Tool>Wasm.available?
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 4. Error Classes
92
+
93
+ Define these inside the module:
94
+
95
+ ```ruby
96
+ module <Tool>Wasm
97
+ class Error < StandardError; end
98
+ class BinaryNotFound < Error; end
99
+ class ExecutionError < Error; end
100
+ end
101
+ ```
102
+
103
+ - `BinaryNotFound` — raised when `run` is called but the binary does not exist.
104
+ - `ExecutionError` — raised when the WASI runtime exits with a non-zero status.
105
+ The error message MUST include the stderr output.
106
+
107
+ ---
108
+
109
+ ## 5. Module Implementation Pattern
110
+
111
+ The main module file (`lib/<tool>_wasm.rb`) must follow this structure:
112
+
113
+ ```ruby
114
+ # frozen_string_literal: true
115
+
116
+ require_relative '<tool>_wasm/version'
117
+ require_relative '<tool>_wasm/downloader'
118
+ require_relative '<tool>_wasm/runner'
119
+
120
+ module <Tool>Wasm
121
+ class Error < StandardError; end
122
+ class BinaryNotFound < Error; end
123
+ class ExecutionError < Error; end
124
+
125
+ DEFAULT_BINARY_PATH = File.join(File.dirname(__FILE__), '<tool>_wasm', '<tool>.wasm').freeze
126
+
127
+ class << self
128
+ attr_writer :binary_path, :runtime
129
+
130
+ def binary_path
131
+ @binary_path || DEFAULT_BINARY_PATH
132
+ end
133
+
134
+ def runtime
135
+ @runtime || 'wasmtime'
136
+ end
137
+
138
+ def download_to_binary_path!
139
+ Downloader.download(to: binary_path)
140
+ end
141
+
142
+ def run(*args, wasm_dir: '.')
143
+ Runner.run(*args, wasm_dir: wasm_dir)
144
+ end
145
+
146
+ def available?
147
+ File.exist?(binary_path)
148
+ end
149
+ end
150
+ end
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 6. Downloader Implementation Pattern
156
+
157
+ The downloader (`lib/<tool>_wasm/downloader.rb`) must:
158
+
159
+ 1. Accept a `to:` keyword argument — the absolute path where the binary will be written.
160
+ 2. Fetch the latest release tag from the GitHub API (`/repos/:owner/:repo/releases/latest`).
161
+ 3. Find the asset named `<tool>.wasm` in the release.
162
+ 4. Stream-download the asset to the target path.
163
+ 5. `chmod 0755` the downloaded file.
164
+ 6. Create intermediate directories with `FileUtils.mkdir_p`.
165
+ 7. Clean up partial files on failure (`FileUtils.rm_f`).
166
+ 8. Use only Ruby stdlib (`net/http`, `json`, `fileutils`, `uri`) — no external dependencies.
167
+
168
+ Constants to adapt per gem:
169
+
170
+ ```ruby
171
+ REPO_OWNER = '<github_owner>'
172
+ REPO_NAME = '<tool>-wasm'
173
+ ASSET_NAME = '<tool>.wasm'
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 7. Runner Implementation Pattern
179
+
180
+ The runner (`lib/<tool>_wasm/runner.rb`) must:
181
+
182
+ 1. Use `Open3.capture3` for proper stdout/stderr/status capture.
183
+ 2. Build the command array (NOT a shell string) for safety:
184
+ ```ruby
185
+ cmd = [
186
+ <Tool>Wasm.runtime,
187
+ 'run',
188
+ '--dir', wasm_dir,
189
+ <Tool>Wasm.binary_path,
190
+ *args
191
+ ]
192
+ ```
193
+ 3. Raise `BinaryNotFound` before executing if `binary_path` does not exist.
194
+ 4. Raise `ExecutionError` with stderr content if exit status is non-zero.
195
+ 5. Return `{ stdout:, stderr:, success: }` on success.
196
+
197
+ ---
198
+
199
+ ## 8. Gemspec Conventions
200
+
201
+ - `required_ruby_version >= 2.7.0`
202
+ - No external runtime dependencies — only Ruby stdlib.
203
+ - Use `git ls-files` to determine included files; exclude build artifacts, tests,
204
+ CI configs, patches, and agent working directories.
205
+ - Include a `post_install_message` explaining that the .wasm binary will be
206
+ downloaded on first use or via `download_to_binary_path!`.
207
+
208
+ ---
209
+
210
+ ## 9. Adapting for a New Binary
211
+
212
+ To create a new gem for a different WASM binary:
213
+
214
+ 1. Copy the `lib/` directory structure.
215
+ 2. Rename all files: replace `pandoc_wasm` with `<tool>_wasm`.
216
+ 3. Rename the module: replace `PandocWasm` with `<Tool>Wasm`.
217
+ 4. Update these constants in `downloader.rb`:
218
+ - `REPO_OWNER`
219
+ - `REPO_NAME`
220
+ - `ASSET_NAME`
221
+ 5. Update `version.rb` with the new gem version.
222
+ 6. Update the gemspec metadata (name, description, homepage, etc.).
223
+ 7. If the WASI runtime needs additional flags (e.g. `--mapdir`, `--env`),
224
+ add them as configurable options on the module and pass them through in the runner.
225
+
226
+ ---
227
+
228
+ ## 10. Testing Strategy
229
+
230
+ Tests use **Minitest** (stdlib, no extra dependency) and run with `rake test`.
231
+ The Rakefile expects `test/**/*_test.rb`.
232
+
233
+ ### 10.1 Test Helper — Module State Reset
234
+
235
+ Because the module stores configuration in instance variables (`@binary_path`,
236
+ `@runtime`), every test file MUST include a helper that saves and restores state:
237
+
238
+ ```ruby
239
+ # test/test_helper.rb
240
+ require 'minitest/autorun'
241
+ require 'tmpdir'
242
+ require 'fileutils'
243
+ require_relative '../lib/<tool>_wasm'
244
+
245
+ module <Tool>WasmTestHelper
246
+ def setup
247
+ @original_binary_path = <Tool>Wasm.instance_variable_get(:@binary_path)
248
+ @original_runtime = <Tool>Wasm.instance_variable_get(:@runtime)
249
+ end
250
+
251
+ def teardown
252
+ <Tool>Wasm.instance_variable_set(:@binary_path, @original_binary_path)
253
+ <Tool>Wasm.instance_variable_set(:@runtime, @original_runtime)
254
+ end
255
+ end
256
+ ```
257
+
258
+ Every test class includes it: `include <Tool>WasmTestHelper`.
259
+
260
+ ### 10.2 Stubbing Conventions
261
+
262
+ - **Runner tests**: Stub `Open3.capture3` with a lambda to capture the command
263
+ array without actually executing anything. Return `[stdout, stderr, status]`
264
+ where `status` is a `Minitest::Mock` responding to `success?` (and
265
+ `exitstatus` on the failure path).
266
+ - **Downloader tests**: Stub the private class methods `get_latest_release_tag`
267
+ and `download_asset` to avoid real HTTP calls.
268
+ - **Main module tests**: Stub `Downloader.download` and `Runner.run` to verify
269
+ delegation without side effects.
270
+
271
+ ### 10.3 Fake Binary Pattern
272
+
273
+ When a test needs the binary to exist (Runner tests), create a temp file:
274
+
275
+ ```ruby
276
+ Dir.mktmpdir do |dir|
277
+ binary = File.join(dir, '<tool>.wasm')
278
+ File.write(binary, 'fake')
279
+ <Tool>Wasm.binary_path = binary
280
+ # ... test logic ...
281
+ end
282
+ ```
283
+
284
+ ### 10.4 Test Checklist
285
+
286
+ When modifying or creating a gem of this type, the test suite MUST cover:
287
+
288
+ **Main module (`test/<tool>_wasm_test.rb`)**:
289
+ - [ ] `binary_path` returns the default path when not configured
290
+ - [ ] `binary_path =` overrides the path
291
+ - [ ] `runtime` returns `"wasmtime"` by default
292
+ - [ ] `runtime =` overrides the runtime
293
+ - [ ] `available?` returns `false` when binary is missing
294
+ - [ ] `available?` returns `true` when binary exists
295
+ - [ ] Error class hierarchy: `BinaryNotFound < Error < StandardError`
296
+ - [ ] `VERSION` matches semver format
297
+ - [ ] `download_to_binary_path!` delegates to `Downloader.download(to: binary_path)`
298
+ - [ ] `run` delegates to `Runner.run` with all arguments forwarded
299
+
300
+ **Runner (`test/runner_test.rb`)**:
301
+ - [ ] Raises `BinaryNotFound` when binary does not exist
302
+ - [ ] Builds the correct command array: `[runtime, "run", "--dir", wasm_dir, binary, *args]`
303
+ - [ ] Uses the configured `runtime` in the command
304
+ - [ ] Returns `{ stdout:, stderr:, success: true }` on success
305
+ - [ ] Raises `ExecutionError` with exit status and stderr on failure
306
+ - [ ] Defaults `wasm_dir` to `"."`
307
+
308
+ **Downloader (`test/downloader_test.rb`)**:
309
+ - [ ] Constants `REPO_OWNER`, `REPO_NAME`, `ASSET_NAME` are defined
310
+ - [ ] `download` method accepts the `to:` keyword argument
311
+ - [ ] `download` raises on network error (re-raises after warning)
312
+ - [ ] `download` expands the target path via `File.expand_path`
313
+ - [ ] `download` returns `true` on success
314
+
315
+ **Integration (`test/integration_test.rb`)**:
316
+ - [ ] Full workflow: configure -> download -> available? -> run succeeds
317
+ - [ ] Run before download raises `BinaryNotFound`
318
+
319
+ ### 10.5 Integration Test Pattern
320
+
321
+ Every gem MUST include an integration test (`test/integration_test.rb`) that
322
+ simulates the complete user journey in a single scenario. This ensures the
323
+ public API methods compose correctly end-to-end.
324
+
325
+ The test walks through these steps in order:
326
+
327
+ 1. **Configure** -- set `binary_path` to a temp directory and `runtime` to a
328
+ non-default value (e.g. `"wazero"`) to prove configuration is respected.
329
+ 2. **Assert not available** -- `available?` returns `false` before download.
330
+ 3. **Download** -- call `download_to_binary_path!` (stub `Downloader.download`
331
+ to write a fake file to the `to:` path).
332
+ 4. **Assert available** -- `available?` returns `true` after download.
333
+ 5. **Run** -- call `run(*args, wasm_dir:)` (stub `Open3.capture3`), then
334
+ inspect the captured command array to verify:
335
+ - `cmd[0]` is the configured runtime
336
+ - `cmd[4]` is the configured `binary_path`
337
+ - `cmd[5..]` matches the args passed in.
338
+
339
+ A second scenario verifies that calling `run` before downloading raises
340
+ `BinaryNotFound`.
341
+
342
+ ```ruby
343
+ class IntegrationTest < Minitest::Test
344
+ include <Tool>WasmTestHelper
345
+
346
+ def test_full_user_workflow
347
+ Dir.mktmpdir do |dir|
348
+ target = File.join(dir, '<tool>.wasm')
349
+
350
+ # 1. Configure
351
+ <Tool>Wasm.binary_path = target
352
+ <Tool>Wasm.runtime = 'wazero'
353
+
354
+ # 2. Not available yet
355
+ refute <Tool>Wasm.available?
356
+
357
+ # 3. Download (stub writes fake file)
358
+ <Tool>Wasm::Downloader.stub(:download, ->(to:) { File.write(to, 'fake'); true }) do
359
+ <Tool>Wasm.download_to_binary_path!
360
+ end
361
+
362
+ # 4. Now available
363
+ assert <Tool>Wasm.available?
364
+
365
+ # 5. Run (stub Open3, capture command)
366
+ captured_cmd = nil
367
+ fake_capture3 = lambda do |*cmd|
368
+ captured_cmd = cmd
369
+ status = Minitest::Mock.new
370
+ status.expect(:success?, true)
371
+ ['', '', status]
372
+ end
373
+
374
+ Open3.stub(:capture3, fake_capture3) do
375
+ result = <Tool>Wasm.run('-o', 'output.pptx', 'input.md', wasm_dir: dir)
376
+ assert result[:success]
377
+ end
378
+
379
+ # 6. Verify command shape
380
+ assert_equal 'wazero', captured_cmd[0]
381
+ assert_equal target, captured_cmd[4]
382
+ assert_equal ['-o', 'output.pptx', 'input.md'], captured_cmd[5..]
383
+ end
384
+ end
385
+
386
+ def test_run_before_download_raises_binary_not_found
387
+ Dir.mktmpdir do |dir|
388
+ <Tool>Wasm.binary_path = File.join(dir, '<tool>.wasm')
389
+ assert_raises(<Tool>Wasm::BinaryNotFound) do
390
+ <Tool>Wasm.run('-o', 'output.pptx', 'input.md', wasm_dir: dir)
391
+ end
392
+ end
393
+ end
394
+ end
395
+ ```
data/README.md CHANGED
@@ -25,10 +25,76 @@ console.log(rgWasmPath); // Path to rg.wasm
25
25
  gem install ripgrep_wasm
26
26
  ```
27
27
 
28
+ Or add it to your `Gemfile`:
29
+
30
+ ```ruby
31
+ gem 'ripgrep_wasm'
32
+ ```
33
+
34
+ #### Quick Start
35
+
28
36
  ```ruby
29
37
  require 'ripgrep_wasm'
30
- puts RipgrepWasm.path # Path to rg.wasm
31
- puts RipgrepWasm.available? # true if binary exists
38
+
39
+ # Download the WASM binary (first time only)
40
+ RipgrepWasm.download_to_binary_path!
41
+
42
+ # Search for a pattern in the current directory
43
+ result = RipgrepWasm.run('-i', 'TODO', '.')
44
+ puts result[:stdout]
45
+ ```
46
+
47
+ #### Configuration
48
+
49
+ ```ruby
50
+ # Get / set the path where rg.wasm is stored
51
+ RipgrepWasm.binary_path # => ".../lib/ripgrep_wasm/rg.wasm" (default)
52
+ RipgrepWasm.binary_path = "/opt/wasm/rg.wasm"
53
+
54
+ # Get / set the WASI runtime executable
55
+ RipgrepWasm.runtime # => "wasmtime" (default)
56
+ RipgrepWasm.runtime = "wasmer"
57
+ ```
58
+
59
+ #### Download
60
+
61
+ ```ruby
62
+ # Downloads rg.wasm from the latest GitHub Release to binary_path.
63
+ # Creates intermediate directories if needed.
64
+ # Returns true on success, raises on failure.
65
+ RipgrepWasm.download_to_binary_path!
66
+ ```
67
+
68
+ #### Execution
69
+
70
+ ```ruby
71
+ # Run ripgrep. All arguments are forwarded to the binary.
72
+ # wasm_dir controls which directory the WASI sandbox can access (default ".").
73
+ result = RipgrepWasm.run('-n', 'pattern', 'src/', wasm_dir: '.')
74
+ # => { stdout: "src/main.rb:12: pattern found\n", stderr: "", success: true }
75
+
76
+ puts result[:stdout]
77
+ ```
78
+
79
+ #### Introspection
80
+
81
+ ```ruby
82
+ RipgrepWasm.available? # => true if rg.wasm exists at binary_path
83
+ ```
84
+
85
+ #### Error Handling
86
+
87
+ ```ruby
88
+ begin
89
+ RipgrepWasm.run('pattern', 'file.txt')
90
+ rescue RipgrepWasm::BinaryNotFound => e
91
+ # rg.wasm is missing -- download it first
92
+ RipgrepWasm.download_to_binary_path!
93
+ retry
94
+ rescue RipgrepWasm::ExecutionError => e
95
+ # Non-zero exit from the runtime (e.g. no matches, bad args)
96
+ warn e.message # includes stderr output
97
+ end
32
98
  ```
33
99
 
34
100
  ### Manual
@@ -10,32 +10,23 @@ module RipgrepWasm
10
10
  REPO_OWNER = 'NathanHimpens'
11
11
  REPO_NAME = 'ripgrep-wasm'
12
12
  ASSET_NAME = 'rg.wasm'
13
-
14
- def self.wasm_path
15
- File.join(File.dirname(__FILE__), ASSET_NAME)
16
- end
17
13
 
18
- # Download rg.wasm from GitHub Releases if it doesn't exist
19
- def self.download_if_needed
20
- return if File.exist?(wasm_path)
21
-
22
- puts "rg.wasm not found. Attempting to download from GitHub Releases..."
23
- download
24
- end
14
+ # Download rg.wasm from the latest GitHub release.
15
+ #
16
+ # @param to [String] absolute path where the binary will be written
17
+ # @return [true] on success
18
+ # @raise [StandardError] on network or API errors
19
+ def self.download(to:)
20
+ target = File.expand_path(to)
21
+ FileUtils.mkdir_p(File.dirname(target))
25
22
 
26
- # Download rg.wasm from the latest GitHub release
27
- def self.download
28
23
  begin
29
24
  tag = get_latest_release_tag
30
- download_asset(tag)
25
+ download_asset(tag, target)
31
26
  rescue StandardError => e
27
+ FileUtils.rm_f(target)
32
28
  warn "Error downloading rg.wasm: #{e.message}"
33
- warn "\nYou can:"
34
- warn "1. Build it yourself following the instructions in README.md"
35
- warn "2. Manually download it from a GitHub release"
36
- warn "3. Copy it from the build directory after compilation"
37
- warn "\n⚠️ Installation will continue, but rg.wasm must be added manually."
38
- false
29
+ raise
39
30
  end
40
31
  end
41
32
 
@@ -44,25 +35,23 @@ module RipgrepWasm
44
35
  # Get the latest release tag from GitHub API
45
36
  def self.get_latest_release_tag
46
37
  uri = URI("https://api.github.com/repos/#{REPO_OWNER}/#{REPO_NAME}/releases/latest")
47
-
38
+
48
39
  http = Net::HTTP.new(uri.host, uri.port)
49
40
  http.use_ssl = true
50
41
  http.read_timeout = 30
51
-
42
+
52
43
  request = Net::HTTP::Get.new(uri)
53
44
  request['User-Agent'] = 'ripgrep-wasm-ruby-downloader'
54
45
  request['Accept'] = 'application/vnd.github.v3+json'
55
-
46
+
56
47
  response = http.request(request)
57
-
48
+
58
49
  case response.code
59
50
  when '200'
60
51
  release = JSON.parse(response.body)
61
52
  release['tag_name']
62
53
  when '404'
63
- # No releases yet, try to use version from gem
64
54
  version = RipgrepWasm::VERSION
65
- puts "No GitHub release found. Using version #{version} from gem."
66
55
  "v#{version}"
67
56
  else
68
57
  raise "GitHub API returned status #{response.code}: #{response.body}"
@@ -70,72 +59,58 @@ module RipgrepWasm
70
59
  end
71
60
 
72
61
  # Download the asset from GitHub Releases
73
- def self.download_asset(tag)
74
- # Get release info to find asset URL
62
+ def self.download_asset(tag, target)
75
63
  uri = URI("https://api.github.com/repos/#{REPO_OWNER}/#{REPO_NAME}/releases/tags/#{tag}")
76
-
64
+
77
65
  http = Net::HTTP.new(uri.host, uri.port)
78
66
  http.use_ssl = true
79
67
  http.read_timeout = 30
80
-
68
+
81
69
  request = Net::HTTP::Get.new(uri)
82
70
  request['User-Agent'] = 'ripgrep-wasm-ruby-downloader'
83
71
  request['Accept'] = 'application/vnd.github.v3+json'
84
-
72
+
85
73
  response = http.request(request)
86
-
74
+
87
75
  case response.code
88
- when '404'
89
- puts "Release #{tag} not found on GitHub. Skipping download."
90
- puts 'You can manually download rg.wasm from the repository or build it yourself.'
91
- return false
92
76
  when '200'
93
- # Continue
77
+ # continue
78
+ when '404'
79
+ raise "Release #{tag} not found on GitHub"
94
80
  else
95
81
  raise "GitHub API returned status #{response.code}: #{response.body}"
96
82
  end
97
-
83
+
98
84
  release = JSON.parse(response.body)
99
85
  asset = release['assets'].find { |a| a['name'] == ASSET_NAME }
100
-
86
+
101
87
  unless asset
102
- puts "Asset #{ASSET_NAME} not found in release #{tag}."
103
- puts 'You can manually download rg.wasm from the repository or build it yourself.'
104
- return false
88
+ raise "Asset #{ASSET_NAME} not found in release #{tag}"
105
89
  end
106
-
107
- # Download the asset
108
- puts "Downloading #{ASSET_NAME} from release #{tag}..."
109
- puts "Size: #{(asset['size'] / 1024.0 / 1024.0).round(2)} MB"
110
-
90
+
111
91
  download_uri = URI(asset['browser_download_url'])
112
92
  download_http = Net::HTTP.new(download_uri.host, download_uri.port)
113
93
  download_http.use_ssl = true
114
- download_http.read_timeout = 300 # 5 minutes for large file
115
-
94
+ download_http.read_timeout = 300
95
+
116
96
  download_request = Net::HTTP::Get.new(download_uri)
117
97
  download_request['User-Agent'] = 'ripgrep-wasm-ruby-downloader'
118
98
  download_request['Accept'] = 'application/octet-stream'
119
-
120
- FileUtils.mkdir_p(File.dirname(wasm_path))
121
-
122
- File.open(wasm_path, 'wb') do |file|
123
- download_http.request(download_request) do |response|
124
- case response.code
99
+
100
+ File.open(target, 'wb') do |file|
101
+ download_http.request(download_request) do |dl_response|
102
+ case dl_response.code
125
103
  when '200'
126
- response.read_body do |chunk|
104
+ dl_response.read_body do |chunk|
127
105
  file.write(chunk)
128
106
  end
129
107
  else
130
- FileUtils.rm_f(wasm_path)
131
- raise "Failed to download asset: #{response.code}"
108
+ raise "Failed to download asset: #{dl_response.code}"
132
109
  end
133
110
  end
134
111
  end
135
-
136
- # Make executable
137
- File.chmod(0o755, wasm_path)
138
- puts "✓ Successfully downloaded #{ASSET_NAME}"
112
+
113
+ File.chmod(0o755, target)
139
114
  true
140
115
  end
141
116
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RipgrepWasm
6
+ class Runner
7
+ # Run the WASM binary via the configured WASI runtime.
8
+ #
9
+ # @param args [Array<String>] arguments passed through to the binary
10
+ # @param wasm_dir [String] directory to expose to the WASI sandbox (default ".")
11
+ # @return [Hash] { stdout: String, stderr: String, success: Boolean }
12
+ # @raise [BinaryNotFound] if the binary does not exist at binary_path
13
+ # @raise [ExecutionError] if the runtime exits with non-zero status
14
+ def self.run(*args, wasm_dir: '.')
15
+ binary = RipgrepWasm.binary_path
16
+
17
+ unless File.exist?(binary)
18
+ raise BinaryNotFound, "WASM binary not found at #{binary}"
19
+ end
20
+
21
+ cmd = [
22
+ RipgrepWasm.runtime,
23
+ 'run',
24
+ '--dir', wasm_dir,
25
+ binary,
26
+ *args
27
+ ]
28
+
29
+ stdout, stderr, status = Open3.capture3(*cmd)
30
+
31
+ unless status.success?
32
+ raise ExecutionError, "Command exited with status #{status.exitstatus}: #{stderr}"
33
+ end
34
+
35
+ { stdout: stdout, stderr: stderr, success: true }
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RipgrepWasm
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/ripgrep_wasm.rb CHANGED
@@ -2,35 +2,36 @@
2
2
 
3
3
  require_relative 'ripgrep_wasm/version'
4
4
  require_relative 'ripgrep_wasm/downloader'
5
+ require_relative 'ripgrep_wasm/runner'
5
6
 
6
7
  module RipgrepWasm
7
8
  class Error < StandardError; end
9
+ class BinaryNotFound < Error; end
10
+ class ExecutionError < Error; end
8
11
 
9
- # Get the path to the rg.wasm binary
10
- #
11
- # @return [String] The absolute path to rg.wasm
12
- def self.path
13
- wasm_path = File.join(File.dirname(__FILE__), 'ripgrep_wasm', 'rg.wasm')
14
-
15
- # If the file doesn't exist, try to download it
16
- unless File.exist?(wasm_path)
17
- Downloader.download_if_needed
12
+ DEFAULT_BINARY_PATH = File.join(File.dirname(__FILE__), 'ripgrep_wasm', 'rg.wasm').freeze
13
+
14
+ class << self
15
+ attr_writer :binary_path, :runtime
16
+
17
+ def binary_path
18
+ @binary_path || DEFAULT_BINARY_PATH
18
19
  end
19
-
20
- wasm_path
21
- end
22
20
 
23
- # Check if rg.wasm is available
24
- #
25
- # @return [Boolean] true if rg.wasm exists
26
- def self.available?
27
- File.exist?(path)
28
- end
21
+ def runtime
22
+ @runtime || 'wasmtime'
23
+ end
29
24
 
30
- # Get the absolute path to rg.wasm
31
- #
32
- # @return [String] The absolute path to rg.wasm
33
- def self.absolute_path
34
- File.expand_path(path)
25
+ def download_to_binary_path!
26
+ Downloader.download(to: binary_path)
27
+ end
28
+
29
+ def run(*args, wasm_dir: '.')
30
+ Runner.run(*args, wasm_dir: wasm_dir)
31
+ end
32
+
33
+ def available?
34
+ File.exist?(binary_path)
35
+ end
35
36
  end
36
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ripgrep_wasm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Himpens
@@ -17,6 +17,7 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - ".npmignore"
20
+ - AGENTS.md
20
21
  - IMPLEMENTATION.md
21
22
  - RALPH_TASK.md
22
23
  - README.md
@@ -27,6 +28,7 @@ files:
27
28
  - lib/ripgrep_wasm.rb
28
29
  - lib/ripgrep_wasm/downloader.rb
29
30
  - lib/ripgrep_wasm/rg.wasm
31
+ - lib/ripgrep_wasm/runner.rb
30
32
  - lib/ripgrep_wasm/version.rb
31
33
  - package.json
32
34
  - rg.wasm