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 +4 -4
- data/AGENTS.md +395 -0
- data/README.md +68 -2
- data/lib/ripgrep_wasm/downloader.rb +37 -62
- data/lib/ripgrep_wasm/runner.rb +38 -0
- data/lib/ripgrep_wasm/version.rb +1 -1
- data/lib/ripgrep_wasm.rb +24 -23
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b2cecf649bbb67e17109c6a368898b8aa036800961105dbd558191dd4ec5572
|
|
4
|
+
data.tar.gz: 3565f9f98ac34d4c5fa79a0442166a09a6b3384b83f1e78410bd68abf94667c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
104
|
+
dl_response.read_body do |chunk|
|
|
127
105
|
file.write(chunk)
|
|
128
106
|
end
|
|
129
107
|
else
|
|
130
|
-
|
|
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
|
-
|
|
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
|
data/lib/ripgrep_wasm/version.rb
CHANGED
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def self.available?
|
|
27
|
-
File.exist?(path)
|
|
28
|
-
end
|
|
21
|
+
def runtime
|
|
22
|
+
@runtime || 'wasmtime'
|
|
23
|
+
end
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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
|