refactor: migrated native tools to WASM-based implementation
- Consolidated grep, find, and image processing into a new `@oh-my-pi/pi-natives` WASM package. - Eliminated subprocess overhead and simplified JS tool implementations. - Updated CI to include Rust workspace, linting, formatting, and WASM build steps. - Documented `Promise.withResolvers()` and `using` declarations for wasm-bindgen.
This commit is contained in:
@@ -18,6 +18,43 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
rust:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with:
|
||||||
|
components: clippy, rustfmt
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy --workspace -- -D warnings
|
||||||
|
- name: Check
|
||||||
|
run: cargo check --workspace
|
||||||
|
|
||||||
|
wasm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Install wasm-pack
|
||||||
|
run: cargo install wasm-pack
|
||||||
|
- name: Install binaryen (wasm-opt)
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y binaryen
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: "1.3"
|
||||||
|
- name: Build WASM
|
||||||
|
run: bun --cwd=packages/natives run build:wasm
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -49,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: test
|
needs: [rust, wasm, test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -44,3 +44,5 @@ __pycache__/
|
|||||||
*.b64.js
|
*.b64.js
|
||||||
|
|
||||||
*.cpuprofile
|
*.cpuprofile
|
||||||
|
|
||||||
|
target/
|
||||||
|
|||||||
@@ -26,6 +26,34 @@ This repo contains multiple packages, but **`packages/coding-agent/`** is the pr
|
|||||||
- Always ask before removing functionality or code that appears to be intentional
|
- Always ask before removing functionality or code that appears to be intentional
|
||||||
- **NEVER build prompts in code** — no inline strings, no template literals, no string concatenation. Prompts live in static `.md` files; use Handlebars for any dynamic content.
|
- **NEVER build prompts in code** — no inline strings, no template literals, no string concatenation. Prompts live in static `.md` files; use Handlebars for any dynamic content.
|
||||||
- **Import static text files via Bun** — use `import content from "./prompt.md" with { type: "text" }` instead of `readFileSync`
|
- **Import static text files via Bun** — use `import content from "./prompt.md" with { type: "text" }` instead of `readFileSync`
|
||||||
|
- **Use `Promise.withResolvers()`** instead of `new Promise((resolve, reject) => ...)` — cleaner, avoids callback nesting, and the resolver functions are properly typed:
|
||||||
|
```typescript
|
||||||
|
// BAD: Verbose, callback nesting
|
||||||
|
const promise = new Promise<string>((resolve, reject) => { ... });
|
||||||
|
|
||||||
|
// GOOD: Clean destructuring, typed resolvers
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## WASM Bindgen Resources
|
||||||
|
|
||||||
|
**Use `using` declarations** for wasm-bindgen objects — they implement `Symbol.dispose`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Manual try/finally with .free()
|
||||||
|
const image = PhotonImage.new_from_byteslice(bytes);
|
||||||
|
try {
|
||||||
|
return image.get_bytes();
|
||||||
|
} finally {
|
||||||
|
image.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Automatic cleanup via Symbol.dispose
|
||||||
|
using image = PhotonImage.new_from_byteslice(bytes);
|
||||||
|
return image.get_bytes();
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies to all wasm-bindgen generated classes (`CompiledPattern`, `PhotonImage`, etc.).
|
||||||
|
|
||||||
## Bun Over Node
|
## Bun Over Node
|
||||||
|
|
||||||
|
|||||||
Generated
+579
@@ -0,0 +1,579 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs_io"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fdeflate"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
|
dependencies = [
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "grep-matcher"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36d7b71093325ab22d780b40d7df3066ae4aebb518ba719d38c697a8228a8023"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "grep-regex"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce0c256c3ad82bcc07b812c15a45ec1d398122e8e15124f96695234db7112ef"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"grep-matcher",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "grep-searcher"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac63295322dc48ebb20a25348147905d816318888e64f531bfc2a2bc0577dc34"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"encoding_rs",
|
||||||
|
"encoding_rs_io",
|
||||||
|
"grep-matcher",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"memmap2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.25.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder-lite",
|
||||||
|
"moxcms",
|
||||||
|
"num-traits",
|
||||||
|
"png",
|
||||||
|
"zune-core",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.85"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.180"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.9.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moxcms"
|
||||||
|
version = "0.7.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pi-natives"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"grep-matcher",
|
||||||
|
"grep-regex",
|
||||||
|
"grep-searcher",
|
||||||
|
"image",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-wasm-bindgen"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi-grep"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"grep-matcher",
|
||||||
|
"grep-regex",
|
||||||
|
"grep-searcher",
|
||||||
|
"ignore",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-core"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-jpeg"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2959ca473aae96a14ecedf501d20b3608d2825ba280d5adb57d651721885b0c2"
|
||||||
|
dependencies = [
|
||||||
|
"zune-core",
|
||||||
|
]
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
authors = ["Can Boluk"]
|
||||||
|
repository = "https://github.com/can1357/oh-my-pi"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
codegen-units = 256
|
||||||
|
incremental = true
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
# Base Lint Levels
|
||||||
|
all = { level = "warn", priority = -1 }
|
||||||
|
correctness = { level = "deny", priority = -1 }
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
perf = { level = "warn", priority = -1 }
|
||||||
|
style = { level = "warn", priority = -1 }
|
||||||
|
suspicious = { level = "deny", priority = -1 }
|
||||||
|
|
||||||
|
# Meta: Lint Attributes
|
||||||
|
allow_attributes_without_reason = "warn"
|
||||||
|
|
||||||
|
# Safety & Unsafe Code
|
||||||
|
undocumented_unsafe_blocks = "warn"
|
||||||
|
|
||||||
|
# Numeric Casts & Conversions
|
||||||
|
cast_lossless = "allow"
|
||||||
|
cast_possible_truncation = "allow"
|
||||||
|
cast_possible_wrap = "allow"
|
||||||
|
cast_precision_loss = "allow"
|
||||||
|
cast_sign_loss = "allow"
|
||||||
|
|
||||||
|
# Functions & Closures
|
||||||
|
inline_always = "allow"
|
||||||
|
must_use_candidate = "allow"
|
||||||
|
needless_pass_by_value = "allow"
|
||||||
|
redundant_closure_for_method_calls = "allow"
|
||||||
|
return_self_not_must_use = "allow"
|
||||||
|
|
||||||
|
# Structs & Types
|
||||||
|
missing_fields_in_debug = "allow"
|
||||||
|
struct_excessive_bools = "allow"
|
||||||
|
|
||||||
|
# Pattern Matching
|
||||||
|
match_same_arms = "allow"
|
||||||
|
match_wildcard_for_single_variants = "allow"
|
||||||
|
option_if_let_else = "allow"
|
||||||
|
|
||||||
|
# Imports & Module Organization
|
||||||
|
enum_glob_use = "allow"
|
||||||
|
items_after_statements = "allow"
|
||||||
|
wildcard_imports = "allow"
|
||||||
|
|
||||||
|
# Variables & Type Inference
|
||||||
|
similar_names = "allow"
|
||||||
|
|
||||||
|
# Literals & Formatting
|
||||||
|
unreadable_literal = "allow"
|
||||||
|
|
||||||
|
# Code Style
|
||||||
|
default_trait_access = "allow"
|
||||||
|
significant_drop_tightening = "allow"
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
missing_errors_doc = "allow"
|
||||||
|
missing_panics_doc = "allow"
|
||||||
|
|
||||||
|
# Complexity
|
||||||
|
too_many_lines = "allow"
|
||||||
+2
-1
@@ -12,7 +12,8 @@
|
|||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noExplicitAny": "off",
|
"noExplicitAny": "off",
|
||||||
"noControlCharactersInRegex": "off",
|
"noControlCharactersInRegex": "off",
|
||||||
"noEmptyInterface": "off"
|
"noEmptyInterface": "off",
|
||||||
|
"noConstEnum": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
"@oh-my-pi/omp-stats": "workspace:*",
|
"@oh-my-pi/omp-stats": "workspace:*",
|
||||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||||
"@oh-my-pi/pi-ai": "workspace:*",
|
"@oh-my-pi/pi-ai": "workspace:*",
|
||||||
|
"@oh-my-pi/pi-natives": "workspace:*",
|
||||||
"@oh-my-pi/pi-tui": "workspace:*",
|
"@oh-my-pi/pi-tui": "workspace:*",
|
||||||
"@oh-my-pi/pi-utils": "workspace:*",
|
"@oh-my-pi/pi-utils": "workspace:*",
|
||||||
"@openai/agents": "^0.4.4",
|
"@openai/agents": "^0.4.4",
|
||||||
@@ -99,6 +100,16 @@
|
|||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/natives": {
|
||||||
|
"name": "@oh-my-pi/pi-natives",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@oh-my-pi/pi-utils": "workspace:*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.0.10",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/pi-utils": {
|
"packages/pi-utils": {
|
||||||
"name": "@oh-my-pi/pi-utils",
|
"name": "@oh-my-pi/pi-utils",
|
||||||
"version": "8.12.3",
|
"version": "8.12.3",
|
||||||
@@ -293,6 +304,8 @@
|
|||||||
|
|
||||||
"@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@workspace:packages/coding-agent"],
|
"@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@workspace:packages/coding-agent"],
|
||||||
|
|
||||||
|
"@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@workspace:packages/natives"],
|
||||||
|
|
||||||
"@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@workspace:packages/tui"],
|
"@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@workspace:packages/tui"],
|
||||||
|
|
||||||
"@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@workspace:packages/pi-utils"],
|
"@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@workspace:packages/pi-utils"],
|
||||||
@@ -475,21 +488,21 @@
|
|||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260128.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260128.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260128.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260128.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260128.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260128.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260128.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260128.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-P7JZ3NpFdH1FqA8TbAKTsufOvykywFAW36H+3ZzUo9PRz/wmTPGkzn/CQ1QM2/YqxWwqLkU96CLeh8onIpj4nw=="],
|
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260129.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260129.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260129.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260129.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260129.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260129.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260129.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260129.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-vYk84B/+e1zfDRRsETQy+xA4Cka4bY+nNKjJgSFr5lCu35EwqOn9WonJeT49pinnpJZApDih+8U9DvSbgpCT9Q=="],
|
||||||
|
|
||||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260128.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rPyH+d+M8NnYA9FOOB9xPtllB2jtDiB7vnLDGYVjE7y8j3XNMEHFYlwoZ5HLS2yLCgwmmcmEUgurJX94V9Iqeg=="],
|
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260129.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sP/8x3P/U3RqDJhnNrQECsD/U0k5/2f3H6Sztyf13Yq7FkBDi2U3ypI8ZpQ6UbzoFIQjD0EFe5+8fckPWJltOA=="],
|
||||||
|
|
||||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260128.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-8zVZNktu8n6+kvWp9x6vb4J2uD51q58mB497ki0Fxf1q2WPTsPFjBUanFZc8K5hltmaoijF1214lN/KNmhVSqg=="],
|
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260129.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-51KL04rbl1s0Xe5gn6/xn0LJtURea0XCuDAlRZ0MSAEqwGVisg2CsaGsI09ICgo5FRHfd1RDFJkfPmFwxxpfDg=="],
|
||||||
|
|
||||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260128.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOKLNjuT5QFkbuJvS3cBTruV22gAllWRqhgFaMYIorT7WcTzEci2S5RfEz1NzYPYZeY4vJssMmr/O8wdPoMpMA=="],
|
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260129.1", "", { "os": "linux", "cpu": "arm" }, "sha512-QplErMgtDiHzbGZG1jm+158yfE4scyvBZlKI3izzgUbTnuaw89RTeoQsd6YIWTQ9m3zGbUzD6F6jU7QsGD6msw=="],
|
||||||
|
|
||||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260128.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xcf3OW/bh6gA+urfG3ofHnfIBhKSqHtFolk2Akd/ZGLiUJik4mimhwlrqx9qus8tGFI3BepdCgfSNhW61fTrhw=="],
|
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260129.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XXbjXv8KoqSF5kP1sQ84iKmoDa79qZWUUDrzDT4lu+hbkQ2Gu0nShstun0Xut2g/1kJtViISc+FFU1C7l9UOtA=="],
|
||||||
|
|
||||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260128.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qr9gTFYXCS83Sro+rBnw3fZIPRMBR9j/9FS5KtPRBOqn4jpiAR64CAE1NDHtTIBYwgPbpd6kvt8Ay5c+S4zxPQ=="],
|
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260129.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eh95rGSBtwBF6PZvDX+tCmChcL1v+gV1J+pAWDVOoTSMERZ5hcQC3XADVTFEMJyG6XVxARUfFcd9N9UnVy1vVA=="],
|
||||||
|
|
||||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260128.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-8Ke5A+MNaiUqaX6nEDpXXmQqfQBBq4CtN6mGEQs5a5QCnyqcnjy13QlJuCxzRd/nvob7lWIxR6kz43Fdn8l5ig=="],
|
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260129.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-DF0FZ3XlsChbLuh3wmVz2UbHPGeyXMmQ8uhaCNtiQig5L79e+12d4Qvft7ffSYsnvbbIYY6u4d4SNxog68JZOQ=="],
|
||||||
|
|
||||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260128.1", "", { "os": "win32", "cpu": "x64" }, "sha512-+r0hFY/fnQUY5z3lcVNWz5j2G2RBxkQVzRAvbQyl2esGt0scy/6dPeP/PrvHFADWoVJkV9DTSBhbmsJfL1inKw=="],
|
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260129.1", "", { "os": "win32", "cpu": "x64" }, "sha512-3Utyo3HIS/FBJw1F6JNIjEYCvnEgPJ61yIBAkQ4YpQobATsNLjs38KR0TkJU4gsHfTOaJ5ZuZIxHc1dskBMSkA=="],
|
||||||
|
|
||||||
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
|
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
|
||||||
|
|
||||||
@@ -879,7 +892,7 @@
|
|||||||
|
|
||||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
"openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
|
"openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="],
|
||||||
|
|
||||||
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
|
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
|
||||||
|
|
||||||
@@ -1127,7 +1140,7 @@
|
|||||||
|
|
||||||
"cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
"cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||||
|
|
||||||
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
|
"cli-truncate/string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="],
|
||||||
|
|
||||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "pi-natives"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde-wasm-bindgen = "0.6"
|
||||||
|
grep-regex = "0.1.14"
|
||||||
|
grep-searcher = "0.1.16"
|
||||||
|
grep-matcher = "0.1.8"
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||||
|
|
||||||
|
# Better panic messages in debug builds
|
||||||
|
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//! Minimal image processing API for resizing and format conversion.
|
||||||
|
//!
|
||||||
|
//! Provides only the subset of functionality needed:
|
||||||
|
//! - Load image from bytes (PNG, JPEG, WebP, GIF)
|
||||||
|
//! - Get dimensions
|
||||||
|
//! - Resize with Lanczos3 filter
|
||||||
|
//! - Export as PNG or JPEG
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use image::{DynamicImage, ImageFormat, ImageReader, imageops::FilterType};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
/// Sampling filter for resize operations.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum SamplingFilter {
|
||||||
|
Nearest = 1,
|
||||||
|
Triangle = 2,
|
||||||
|
CatmullRom = 3,
|
||||||
|
Gaussian = 4,
|
||||||
|
Lanczos3 = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SamplingFilter> for FilterType {
|
||||||
|
fn from(filter: SamplingFilter) -> Self {
|
||||||
|
match filter {
|
||||||
|
SamplingFilter::Nearest => FilterType::Nearest,
|
||||||
|
SamplingFilter::Triangle => FilterType::Triangle,
|
||||||
|
SamplingFilter::CatmullRom => FilterType::CatmullRom,
|
||||||
|
SamplingFilter::Gaussian => FilterType::Gaussian,
|
||||||
|
SamplingFilter::Lanczos3 => FilterType::Lanczos3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Image container for WASM interop.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct PhotonImage {
|
||||||
|
img: DynamicImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl PhotonImage {
|
||||||
|
/// Create a new PhotonImage from encoded image bytes (PNG, JPEG, WebP, GIF).
|
||||||
|
#[wasm_bindgen(js_name = new_from_byteslice)]
|
||||||
|
pub fn new_from_byteslice(bytes: &[u8]) -> Result<PhotonImage, JsValue> {
|
||||||
|
let reader = ImageReader::new(Cursor::new(bytes))
|
||||||
|
.with_guessed_format()
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to detect image format: {e}")))?;
|
||||||
|
|
||||||
|
let img = reader
|
||||||
|
.decode()
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to decode image: {e}")))?;
|
||||||
|
|
||||||
|
Ok(PhotonImage { img })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the width of the image.
|
||||||
|
#[wasm_bindgen(js_name = get_width)]
|
||||||
|
pub fn get_width(&self) -> u32 {
|
||||||
|
self.img.width()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the height of the image.
|
||||||
|
#[wasm_bindgen(js_name = get_height)]
|
||||||
|
pub fn get_height(&self) -> u32 {
|
||||||
|
self.img.height()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export image as PNG bytes.
|
||||||
|
#[wasm_bindgen(js_name = get_bytes)]
|
||||||
|
pub fn get_bytes(&self) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
self.img
|
||||||
|
.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Png)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to encode PNG: {e}")))?;
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export image as JPEG bytes with specified quality (0-100).
|
||||||
|
#[wasm_bindgen(js_name = get_bytes_jpeg)]
|
||||||
|
pub fn get_bytes_jpeg(&self, quality: u8) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buffer, quality);
|
||||||
|
self.img
|
||||||
|
.write_with_encoder(encoder)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to encode JPEG: {e}")))?;
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize an image to the specified dimensions.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn resize(
|
||||||
|
image: &PhotonImage,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
filter: SamplingFilter,
|
||||||
|
) -> PhotonImage {
|
||||||
|
let resized = image.img.resize_exact(width, height, filter.into());
|
||||||
|
PhotonImage { img: resized }
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
//! WASM module for regex matching using ripgrep's engine.
|
||||||
|
//!
|
||||||
|
//! This module provides pure regex matching - no filesystem access.
|
||||||
|
//! The JS side handles directory walking and file reading, then passes
|
||||||
|
//! content here for matching.
|
||||||
|
|
||||||
|
pub mod image;
|
||||||
|
|
||||||
|
use std::io::{self, Cursor};
|
||||||
|
|
||||||
|
use grep_matcher::Matcher;
|
||||||
|
use grep_regex::RegexMatcherBuilder;
|
||||||
|
use grep_searcher::{
|
||||||
|
BinaryDetection, Searcher, SearcherBuilder, Sink, SinkContext, SinkContextKind, SinkMatch,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
|
fn set_panic_hook() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for searching file content.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchOptions {
|
||||||
|
/// Regex pattern to search for.
|
||||||
|
pub pattern: String,
|
||||||
|
/// Case-insensitive search.
|
||||||
|
#[serde(default)]
|
||||||
|
pub ignore_case: bool,
|
||||||
|
/// Enable multiline matching.
|
||||||
|
#[serde(default)]
|
||||||
|
pub multiline: bool,
|
||||||
|
/// Maximum number of matches to return.
|
||||||
|
#[serde(default)]
|
||||||
|
pub max_count: Option<u64>,
|
||||||
|
/// Skip first N matches.
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: Option<u64>,
|
||||||
|
/// Lines of context before/after matches.
|
||||||
|
#[serde(default)]
|
||||||
|
pub context: Option<usize>,
|
||||||
|
/// Truncate lines longer than this (characters).
|
||||||
|
#[serde(default)]
|
||||||
|
pub max_columns: Option<usize>,
|
||||||
|
/// Output mode (content or count).
|
||||||
|
#[serde(default)]
|
||||||
|
pub mode: SearchMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum SearchMode {
|
||||||
|
#[default]
|
||||||
|
Content,
|
||||||
|
Count,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A context line (before or after a match).
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ContextLine {
|
||||||
|
pub line_number: u64,
|
||||||
|
pub line: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single match in the content.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Match {
|
||||||
|
/// 1-indexed line number.
|
||||||
|
pub line_number: u64,
|
||||||
|
/// The matched line content.
|
||||||
|
pub line: String,
|
||||||
|
/// Context lines before the match.
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub context_before: Vec<ContextLine>,
|
||||||
|
/// Context lines after the match.
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub context_after: Vec<ContextLine>,
|
||||||
|
/// Whether the line was truncated.
|
||||||
|
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of searching content.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchResult {
|
||||||
|
/// All matches found.
|
||||||
|
pub matches: Vec<Match>,
|
||||||
|
/// Total number of matches (may exceed `matches.len()` due to offset/limit).
|
||||||
|
pub match_count: u64,
|
||||||
|
/// Whether the limit was reached.
|
||||||
|
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub limit_reached: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error result.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ErrorResult {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MatchCollector {
|
||||||
|
matches: Vec<CollectedMatch>,
|
||||||
|
match_count: u64,
|
||||||
|
collected_count: u64,
|
||||||
|
max_count: Option<u64>,
|
||||||
|
offset: u64,
|
||||||
|
skipped: u64,
|
||||||
|
limit_reached: bool,
|
||||||
|
context_before: Vec<ContextLine>,
|
||||||
|
max_columns: Option<usize>,
|
||||||
|
collect_matches: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CollectedMatch {
|
||||||
|
line_number: u64,
|
||||||
|
line: String,
|
||||||
|
context_before: Vec<ContextLine>,
|
||||||
|
context_after: Vec<ContextLine>,
|
||||||
|
truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatchCollector {
|
||||||
|
const fn new(
|
||||||
|
max_count: Option<u64>,
|
||||||
|
offset: u64,
|
||||||
|
max_columns: Option<usize>,
|
||||||
|
collect_matches: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
matches: Vec::new(),
|
||||||
|
match_count: 0,
|
||||||
|
collected_count: 0,
|
||||||
|
max_count,
|
||||||
|
offset,
|
||||||
|
skipped: 0,
|
||||||
|
limit_reached: false,
|
||||||
|
context_before: Vec::new(),
|
||||||
|
max_columns,
|
||||||
|
collect_matches,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_line(&self, line: &str) -> (String, bool) {
|
||||||
|
match self.max_columns {
|
||||||
|
Some(max) if line.len() > max => {
|
||||||
|
let truncated = format!("{}...", &line[..max.saturating_sub(3)]);
|
||||||
|
(truncated, true)
|
||||||
|
},
|
||||||
|
_ => (line.to_string(), false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sink for MatchCollector {
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
fn matched(&mut self, _searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
|
||||||
|
self.match_count += 1;
|
||||||
|
|
||||||
|
// If we already hit the limit, stop now (after-context for previous match was collected)
|
||||||
|
if self.limit_reached {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.skipped < self.offset {
|
||||||
|
self.skipped += 1;
|
||||||
|
self.context_before.clear();
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.collect_matches {
|
||||||
|
let raw_line = String::from_utf8_lossy(mat.bytes()).trim_end().to_string();
|
||||||
|
let (line, truncated) = self.truncate_line(&raw_line);
|
||||||
|
let line_number = mat.line_number().unwrap_or(0);
|
||||||
|
|
||||||
|
self.matches.push(CollectedMatch {
|
||||||
|
line_number,
|
||||||
|
line,
|
||||||
|
context_before: std::mem::take(&mut self.context_before),
|
||||||
|
context_after: Vec::new(),
|
||||||
|
truncated,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.context_before.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.collected_count += 1;
|
||||||
|
|
||||||
|
// Mark limit reached but don't stop yet - allow after-context to be collected
|
||||||
|
if let Some(max) = self.max_count
|
||||||
|
&& self.collected_count >= max
|
||||||
|
{
|
||||||
|
self.limit_reached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context(&mut self, _searcher: &Searcher, ctx: &SinkContext<'_>) -> Result<bool, Self::Error> {
|
||||||
|
if !self.collect_matches {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_line = String::from_utf8_lossy(ctx.bytes()).trim_end().to_string();
|
||||||
|
let (line, _) = self.truncate_line(&raw_line);
|
||||||
|
let line_number = ctx.line_number().unwrap_or(0);
|
||||||
|
|
||||||
|
match ctx.kind() {
|
||||||
|
SinkContextKind::Before => {
|
||||||
|
self.context_before.push(ContextLine { line_number, line });
|
||||||
|
},
|
||||||
|
SinkContextKind::After => {
|
||||||
|
if let Some(last_match) = self.matches.last_mut() {
|
||||||
|
last_match
|
||||||
|
.context_after
|
||||||
|
.push(ContextLine { line_number, line });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SinkContextKind::Other => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A compiled regex matcher that can be reused across multiple searches.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct CompiledPattern {
|
||||||
|
matcher: grep_regex::RegexMatcher,
|
||||||
|
context: usize,
|
||||||
|
max_columns: Option<usize>,
|
||||||
|
mode: SearchMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl CompiledPattern {
|
||||||
|
/// Compile a regex pattern for reuse.
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(options: JsValue) -> Result<Self, JsValue> {
|
||||||
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
|
set_panic_hook();
|
||||||
|
|
||||||
|
let opts: SearchOptions = serde_wasm_bindgen::from_value(options)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid options: {e}")))?;
|
||||||
|
|
||||||
|
let matcher = RegexMatcherBuilder::new()
|
||||||
|
.case_insensitive(opts.ignore_case)
|
||||||
|
.multi_line(opts.multiline)
|
||||||
|
.build(&opts.pattern)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Regex error: {e}")))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
matcher,
|
||||||
|
context: opts.context.unwrap_or(0),
|
||||||
|
max_columns: opts.max_columns,
|
||||||
|
mode: opts.mode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search content using this compiled pattern.
|
||||||
|
/// Returns matches as a JS object.
|
||||||
|
pub fn search(&self, content: &str, max_count: Option<u32>, offset: Option<u32>) -> JsValue {
|
||||||
|
let context_lines = self.context;
|
||||||
|
let mut searcher = SearcherBuilder::new()
|
||||||
|
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||||
|
.line_number(true)
|
||||||
|
.before_context(context_lines)
|
||||||
|
.after_context(context_lines)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut collector = MatchCollector::new(
|
||||||
|
max_count.map(|n| n as u64),
|
||||||
|
offset.unwrap_or(0) as u64,
|
||||||
|
self.max_columns,
|
||||||
|
self.mode == SearchMode::Content,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = Cursor::new(content.as_bytes());
|
||||||
|
if let Err(e) = searcher.search_reader(&self.matcher, cursor, &mut collector) {
|
||||||
|
return serde_wasm_bindgen::to_value(&ErrorResult { error: e.to_string() })
|
||||||
|
.unwrap_or(JsValue::NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = SearchResult {
|
||||||
|
matches: collector
|
||||||
|
.matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| Match {
|
||||||
|
line_number: m.line_number,
|
||||||
|
line: m.line,
|
||||||
|
context_before: m.context_before,
|
||||||
|
context_after: m.context_after,
|
||||||
|
truncated: m.truncated,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
match_count: collector.match_count,
|
||||||
|
limit_reached: collector.limit_reached,
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if content has any matches (faster than full search).
|
||||||
|
pub fn has_match(&self, content: &str) -> bool {
|
||||||
|
self.matcher.is_match(content.as_bytes()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search bytes directly (avoids UTF-16 to UTF-8 conversion).
|
||||||
|
/// Use with `Bun.mmap()` for best performance.
|
||||||
|
pub fn search_bytes(
|
||||||
|
&self,
|
||||||
|
content: &[u8],
|
||||||
|
max_count: Option<u32>,
|
||||||
|
offset: Option<u32>,
|
||||||
|
) -> JsValue {
|
||||||
|
let context_lines = self.context;
|
||||||
|
let mut searcher = SearcherBuilder::new()
|
||||||
|
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||||
|
.line_number(true)
|
||||||
|
.before_context(context_lines)
|
||||||
|
.after_context(context_lines)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut collector = MatchCollector::new(
|
||||||
|
max_count.map(|n| n as u64),
|
||||||
|
offset.unwrap_or(0) as u64,
|
||||||
|
self.max_columns,
|
||||||
|
self.mode == SearchMode::Content,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = Cursor::new(content);
|
||||||
|
if let Err(e) = searcher.search_reader(&self.matcher, cursor, &mut collector) {
|
||||||
|
return serde_wasm_bindgen::to_value(&ErrorResult { error: e.to_string() })
|
||||||
|
.unwrap_or(JsValue::NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = SearchResult {
|
||||||
|
matches: collector
|
||||||
|
.matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| Match {
|
||||||
|
line_number: m.line_number,
|
||||||
|
line: m.line,
|
||||||
|
context_before: m.context_before,
|
||||||
|
context_after: m.context_after,
|
||||||
|
truncated: m.truncated,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
match_count: collector.match_count,
|
||||||
|
limit_reached: collector.limit_reached,
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if bytes have any matches (faster than full search).
|
||||||
|
pub fn has_match_bytes(&self, content: &[u8]) -> bool {
|
||||||
|
self.matcher.is_match(content).unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search content for a pattern (one-shot, compiles pattern each time).
|
||||||
|
/// For repeated searches with the same pattern, use [`CompiledPattern`].
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn search(content: &str, options: JsValue) -> JsValue {
|
||||||
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
|
set_panic_hook();
|
||||||
|
|
||||||
|
let opts: SearchOptions = match serde_wasm_bindgen::from_value(options) {
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(e) => {
|
||||||
|
return serde_wasm_bindgen::to_value(&ErrorResult {
|
||||||
|
error: format!("Invalid options: {e}"),
|
||||||
|
})
|
||||||
|
.unwrap_or(JsValue::NULL);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let matcher = match RegexMatcherBuilder::new()
|
||||||
|
.case_insensitive(opts.ignore_case)
|
||||||
|
.multi_line(opts.multiline)
|
||||||
|
.build(&opts.pattern)
|
||||||
|
{
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
return serde_wasm_bindgen::to_value(&ErrorResult { error: format!("Regex error: {e}") })
|
||||||
|
.unwrap_or(JsValue::NULL);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let context_lines = opts.context.unwrap_or(0);
|
||||||
|
let mut searcher = SearcherBuilder::new()
|
||||||
|
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||||
|
.line_number(true)
|
||||||
|
.before_context(context_lines)
|
||||||
|
.after_context(context_lines)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut collector = MatchCollector::new(
|
||||||
|
opts.max_count,
|
||||||
|
opts.offset.unwrap_or(0),
|
||||||
|
opts.max_columns,
|
||||||
|
opts.mode == SearchMode::Content,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = Cursor::new(content.as_bytes());
|
||||||
|
if let Err(e) = searcher.search_reader(&matcher, cursor, &mut collector) {
|
||||||
|
return serde_wasm_bindgen::to_value(&ErrorResult { error: e.to_string() })
|
||||||
|
.unwrap_or(JsValue::NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = SearchResult {
|
||||||
|
matches: collector
|
||||||
|
.matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| Match {
|
||||||
|
line_number: m.line_number,
|
||||||
|
line: m.line,
|
||||||
|
context_before: m.context_before,
|
||||||
|
context_after: m.context_after,
|
||||||
|
truncated: m.truncated,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
match_count: collector.match_count,
|
||||||
|
limit_reached: collector.limit_reached,
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quick check if content matches a pattern.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn has_match(
|
||||||
|
content: &str,
|
||||||
|
pattern: &str,
|
||||||
|
ignore_case: bool,
|
||||||
|
multiline: bool,
|
||||||
|
) -> Result<bool, JsValue> {
|
||||||
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
|
set_panic_hook();
|
||||||
|
|
||||||
|
let matcher = RegexMatcherBuilder::new()
|
||||||
|
.case_insensitive(ignore_case)
|
||||||
|
.multi_line(multiline)
|
||||||
|
.build(pattern)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Regex error: {e}")))?;
|
||||||
|
|
||||||
|
Ok(matcher.is_match(content.as_bytes()).unwrap_or(false))
|
||||||
|
}
|
||||||
+6
-1
@@ -10,8 +10,13 @@
|
|||||||
"dev": "bun packages/coding-agent/src/cli.ts",
|
"dev": "bun packages/coding-agent/src/cli.ts",
|
||||||
"stats": "bun --cwd=packages/coding-agent src/cli.ts stats",
|
"stats": "bun --cwd=packages/coding-agent src/cli.ts stats",
|
||||||
"check": "biome check --write . && tsgo -p tsconfig.json && tsgo -p packages/stats/tsconfig.client.json",
|
"check": "biome check --write . && tsgo -p tsconfig.json && tsgo -p packages/stats/tsconfig.client.json",
|
||||||
"test": "bun --cwd=packages/ai run test && bun --cwd=packages/agent run test && bun --cwd=packages/tui run test && bun --cwd=packages/coding-agent run test",
|
"test": "bun --cwd=packages/ai run test && bun --cwd=packages/agent run test && bun --cwd=packages/tui run test && bun --cwd=packages/natives run test && bun --cwd=packages/coding-agent run test",
|
||||||
"fix": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts",
|
"fix": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts",
|
||||||
|
"rust:check": "cargo check --workspace",
|
||||||
|
"rust:clippy": "cargo clippy --workspace -- -D warnings",
|
||||||
|
"rust:fmt": "cargo fmt --all -- --check",
|
||||||
|
"rust:fmt:fix": "cargo fmt --all",
|
||||||
|
"rust:lint": "cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings",
|
||||||
"bench:gen-fixtures": "bun run packages/react-edit-benchmark/generate.ts --react-dir /tmp/react-source --count-per-type 3",
|
"bench:gen-fixtures": "bun run packages/react-edit-benchmark/generate.ts --react-dir /tmp/react-source --count-per-type 3",
|
||||||
"bench:edit": "bun --cwd=packages/react-edit-benchmark run start",
|
"bench:edit": "bun --cwd=packages/react-edit-benchmark run start",
|
||||||
"version:patch": "npm version patch -ws --no-git-tag-version && bun scripts/sync-versions.ts && rm -rf node_modules packages/*/node_modules bun.lock && bun install",
|
"version:patch": "npm version patch -ws --no-git-tag-version && bun scripts/sync-versions.ts && rm -rf node_modules packages/*/node_modules bun.lock && bun install",
|
||||||
|
|||||||
@@ -58,10 +58,6 @@
|
|||||||
"types": "./src/internal-urls/*.ts",
|
"types": "./src/internal-urls/*.ts",
|
||||||
"import": "./src/internal-urls/*.ts"
|
"import": "./src/internal-urls/*.ts"
|
||||||
},
|
},
|
||||||
"./vendor/photon/*": {
|
|
||||||
"types": "./src/vendor/photon/*",
|
|
||||||
"import": "./src/vendor/photon/*"
|
|
||||||
},
|
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./src/*.ts",
|
"types": "./src/*.ts",
|
||||||
"import": "./src/*.ts"
|
"import": "./src/*.ts"
|
||||||
@@ -86,6 +82,7 @@
|
|||||||
"@oh-my-pi/omp-stats": "workspace:*",
|
"@oh-my-pi/omp-stats": "workspace:*",
|
||||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||||
"@oh-my-pi/pi-ai": "workspace:*",
|
"@oh-my-pi/pi-ai": "workspace:*",
|
||||||
|
"@oh-my-pi/pi-natives": "workspace:*",
|
||||||
"@oh-my-pi/pi-tui": "workspace:*",
|
"@oh-my-pi/pi-tui": "workspace:*",
|
||||||
"@oh-my-pi/pi-utils": "workspace:*",
|
"@oh-my-pi/pi-utils": "workspace:*",
|
||||||
"@openai/agents": "^0.4.4",
|
"@openai/agents": "^0.4.4",
|
||||||
|
|||||||
@@ -93,11 +93,6 @@ export function getToolsDir(): string {
|
|||||||
return path.join(getAgentDir(), "tools");
|
return path.join(getAgentDir(), "tools");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get path to managed binaries directory (fd, rg) */
|
|
||||||
export function getBinDir(): string {
|
|
||||||
return path.join(getAgentDir(), "bin");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get path to slash commands directory */
|
/** Get path to slash commands directory */
|
||||||
export function getCommandsDir(): string {
|
export function getCommandsDir(): string {
|
||||||
return path.join(getAgentDir(), "commands");
|
return path.join(getAgentDir(), "commands");
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
|||||||
|
|
||||||
// Check external tools
|
// Check external tools
|
||||||
const tools = [
|
const tools = [
|
||||||
{ name: "fd", description: "File finder" },
|
|
||||||
{ name: "rg", description: "Ripgrep" },
|
|
||||||
{ name: "sd", description: "Find-replace" },
|
{ name: "sd", description: "Find-replace" },
|
||||||
{ name: "sg", description: "AST-grep" },
|
{ name: "sg", description: "AST-grep" },
|
||||||
{ name: "git", description: "Version control" },
|
{ name: "git", description: "Version control" },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
|
import { getAgentDbPath, getAgentDir } from "./config";
|
||||||
import { AgentStorage } from "./session/agent-storage";
|
import { AgentStorage } from "./session/agent-storage";
|
||||||
import type { AuthCredential } from "./session/auth-storage";
|
import type { AuthCredential } from "./session/auth-storage";
|
||||||
|
|
||||||
@@ -144,50 +144,6 @@ export async function migrateSessionsFromAgentRoot(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move fd/rg binaries from tools/ to bin/ if they exist.
|
|
||||||
*/
|
|
||||||
async function migrateToolsToBin(): Promise<void> {
|
|
||||||
const agentDir = getAgentDir();
|
|
||||||
const toolsDir = path.join(agentDir, "tools");
|
|
||||||
const binDir = getBinDir();
|
|
||||||
|
|
||||||
if (!fs.existsSync(toolsDir)) return;
|
|
||||||
|
|
||||||
const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
|
|
||||||
let movedAny = false;
|
|
||||||
|
|
||||||
for (const bin of binaries) {
|
|
||||||
const oldPath = path.join(toolsDir, bin);
|
|
||||||
const newPath = path.join(binDir, bin);
|
|
||||||
if (!fs.existsSync(oldPath)) continue;
|
|
||||||
|
|
||||||
if (!fs.existsSync(binDir)) {
|
|
||||||
await fs.promises.mkdir(binDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(newPath)) {
|
|
||||||
try {
|
|
||||||
await fs.promises.rename(oldPath, newPath);
|
|
||||||
movedAny = true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Target exists, just delete the old one
|
|
||||||
try {
|
|
||||||
await fs.promises.rm(oldPath, { force: true });
|
|
||||||
} catch {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (movedAny) {
|
|
||||||
console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run all migrations. Called once on startup.
|
* Run all migrations. Called once on startup.
|
||||||
*
|
*
|
||||||
@@ -201,7 +157,6 @@ export async function runMigrations(_cwd: string): Promise<{
|
|||||||
// Then: run data migrations
|
// Then: run data migrations
|
||||||
const migratedAuthProviders = await migrateAuthToAgentDb();
|
const migratedAuthProviders = await migrateAuthToAgentDb();
|
||||||
await migrateSessionsFromAgentRoot();
|
await migrateSessionsFromAgentRoot();
|
||||||
await migrateToolsToBin();
|
|
||||||
|
|
||||||
return { migratedAuthProviders, deprecationWarnings: [] };
|
return { migratedAuthProviders, deprecationWarnings: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { globPaths } from "@oh-my-pi/pi-utils";
|
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
||||||
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { contextFileCapability } from "./capability/context-file";
|
import { contextFileCapability } from "./capability/context-file";
|
||||||
@@ -201,17 +202,21 @@ type ProjectTreeScan = {
|
|||||||
const GLOB_TIMEOUT_MS = 5000;
|
const GLOB_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan project tree using fs.promises.glob with exclusion filters.
|
* Scan project tree using ripgrep-wasm find with exclusion filters.
|
||||||
* Returns null if glob fails.
|
* Returns null if scan fails.
|
||||||
*/
|
*/
|
||||||
async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan | null> {
|
async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan | null> {
|
||||||
let entries: string[];
|
let entries: string[];
|
||||||
|
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
||||||
try {
|
try {
|
||||||
entries = await globPaths("**/*", {
|
const result = await untilAborted(timeoutSignal, () =>
|
||||||
cwd: root,
|
wasmFind({
|
||||||
gitignore: true,
|
pattern: "**/*",
|
||||||
timeoutMs: GLOB_TIMEOUT_MS,
|
path: root,
|
||||||
});
|
fileType: "file",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
entries = result.matches.map(match => match.path).filter(entry => entry.length > 0);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -222,7 +227,7 @@ async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan |
|
|||||||
dirContents.set(root, new Map());
|
dirContents.set(root, new Map());
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const filePath = entry.trim();
|
const filePath = entry;
|
||||||
if (!filePath) continue;
|
if (!filePath) continue;
|
||||||
const absolutePath = path.join(root, filePath);
|
const absolutePath = path.join(root, filePath);
|
||||||
// Check static ignores on path components
|
// Check static ignores on path components
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||||
|
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
||||||
import type { Component } from "@oh-my-pi/pi-tui";
|
import type { Component } from "@oh-my-pi/pi-tui";
|
||||||
import { Text } from "@oh-my-pi/pi-tui";
|
import { Text } from "@oh-my-pi/pi-tui";
|
||||||
import { globPaths, isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
||||||
import type { Static } from "@sinclair/typebox";
|
import type { Static } from "@sinclair/typebox";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||||
@@ -169,22 +170,26 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let lines: string[];
|
let lines: string[];
|
||||||
|
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
||||||
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
||||||
try {
|
try {
|
||||||
lines = await globPaths(globPattern, {
|
const result = await untilAborted(combinedSignal, () =>
|
||||||
cwd: searchPath,
|
wasmFind({
|
||||||
gitignore: true,
|
pattern: globPattern,
|
||||||
dot: includeHidden,
|
path: searchPath,
|
||||||
signal,
|
fileType: "file",
|
||||||
timeoutMs: GLOB_TIMEOUT_MS,
|
hidden: includeHidden,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
lines = result.matches.map(match => match.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
if (timeoutSignal.aborted && !signal?.aborted) {
|
||||||
|
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
||||||
|
throw new ToolError(`find timed out after ${timeoutSeconds}s`);
|
||||||
|
}
|
||||||
throw new ToolAbortError();
|
throw new ToolAbortError();
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.name === "TimeoutError") {
|
|
||||||
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
|
||||||
throw new ToolError(`glob timed out after ${timeoutSeconds}s`);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,13 +202,13 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|||||||
|
|
||||||
for (const rawLine of lines) {
|
for (const rawLine of lines) {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
const line = rawLine.replace(/\r$/, "").trim();
|
const line = rawLine;
|
||||||
if (!line) {
|
if (!line) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
||||||
let relativePath = line.replace(/\\/g, "/");
|
let relativePath = line;
|
||||||
|
|
||||||
let mtimeMs = 0;
|
let mtimeMs = 0;
|
||||||
let isDirectory = false;
|
let isDirectory = false;
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import * as nodePath from "node:path";
|
import * as nodePath from "node:path";
|
||||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||||
import { StringEnum } from "@oh-my-pi/pi-ai";
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
||||||
|
import { type GrepMatch as WasmGrepMatch, grep as wasmGrep } from "@oh-my-pi/pi-natives";
|
||||||
import type { Component } from "@oh-my-pi/pi-tui";
|
import type { Component } from "@oh-my-pi/pi-tui";
|
||||||
import { Text } from "@oh-my-pi/pi-tui";
|
import { Text } from "@oh-my-pi/pi-tui";
|
||||||
import { ptree } from "@oh-my-pi/pi-utils";
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
||||||
import { type Static, Type } from "@sinclair/typebox";
|
import { type Static, Type } from "@sinclair/typebox";
|
||||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||||
import type { Theme } from "../modes/theme/theme";
|
import type { Theme } from "../modes/theme/theme";
|
||||||
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
||||||
import { renderFileList, renderStatusLine, renderTreeList } from "../tui";
|
import { renderFileList, renderStatusLine, renderTreeList } from "../tui";
|
||||||
import { ensureTool } from "../utils/tools-manager";
|
|
||||||
import { untilAborted } from "../utils/utils";
|
|
||||||
import type { ToolSession } from ".";
|
import type { ToolSession } from ".";
|
||||||
import { applyListLimit } from "./list-limit";
|
|
||||||
import type { OutputMeta } from "./output-meta";
|
import type { OutputMeta } from "./output-meta";
|
||||||
import { resolveToCwd } from "./path-utils";
|
import { resolveToCwd } from "./path-utils";
|
||||||
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
||||||
import { ToolAbortError, ToolError } from "./tool-errors";
|
import { ToolError } from "./tool-errors";
|
||||||
import { toolResult } from "./tool-result";
|
import { toolResult } from "./tool-result";
|
||||||
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "./truncate";
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "./truncate";
|
||||||
|
|
||||||
const grepSchema = Type.Object({
|
const grepSchema = Type.Object({
|
||||||
pattern: Type.String({ description: "Regex pattern to search for" }),
|
pattern: Type.String({ description: "Regex pattern to search for" }),
|
||||||
@@ -27,7 +25,7 @@ const grepSchema = Type.Object({
|
|||||||
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
||||||
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
||||||
output_mode: Type.Optional(
|
output_mode: Type.Optional(
|
||||||
StringEnum(["files_with_matches", "content", "count"], {
|
StringEnum(["filesWithMatches", "content", "count"], {
|
||||||
description: "Output format (default: files_with_matches)",
|
description: "Output format (default: files_with_matches)",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -47,76 +45,22 @@ export interface GrepToolDetails {
|
|||||||
resultLimitReached?: number;
|
resultLimitReached?: number;
|
||||||
linesTruncated?: boolean;
|
linesTruncated?: boolean;
|
||||||
meta?: OutputMeta;
|
meta?: OutputMeta;
|
||||||
// Fields for TUI rendering
|
|
||||||
scopePath?: string;
|
scopePath?: string;
|
||||||
matchCount?: number;
|
matchCount?: number;
|
||||||
fileCount?: number;
|
fileCount?: number;
|
||||||
files?: string[];
|
files?: string[];
|
||||||
fileMatches?: Array<{ path: string; count: number }>;
|
fileMatches?: Array<{ path: string; count: number }>;
|
||||||
mode?: "content" | "files_with_matches" | "count";
|
mode?: "content" | "filesWithMatches" | "count";
|
||||||
truncated?: boolean;
|
truncated?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RgResult {
|
|
||||||
stdout: string;
|
|
||||||
stderr: string;
|
|
||||||
exitCode: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run rg command and capture output.
|
|
||||||
*
|
|
||||||
* @throws ToolAbortError if signal is aborted
|
|
||||||
*/
|
|
||||||
export async function runRg(
|
|
||||||
rgPath: string,
|
|
||||||
args: string[],
|
|
||||||
options?: { signal?: AbortSignal; timeoutMs?: number },
|
|
||||||
): Promise<RgResult> {
|
|
||||||
const timeoutSeconds = options?.timeoutMs ? Math.max(1, Math.round(options.timeoutMs / 1000)) : undefined;
|
|
||||||
const timeoutMessage = timeoutSeconds ? `rg timed out after ${timeoutSeconds}s` : "rg timed out";
|
|
||||||
|
|
||||||
const result = await ptree.exec([rgPath, ...args], {
|
|
||||||
signal: options?.signal,
|
|
||||||
timeout: options?.timeoutMs,
|
|
||||||
allowNonZero: true,
|
|
||||||
allowAbort: true,
|
|
||||||
stderr: "buffer",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.exitError instanceof ptree.TimeoutError) {
|
|
||||||
throw new ToolError(timeoutMessage);
|
|
||||||
}
|
|
||||||
if (result.exitError?.aborted) {
|
|
||||||
throw new ToolAbortError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
stdout: result.stdout,
|
|
||||||
stderr: result.stderr,
|
|
||||||
exitCode: result.exitCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pluggable operations for the grep tool.
|
|
||||||
* Override these to delegate search to remote systems (e.g., SSH).
|
|
||||||
*/
|
|
||||||
export interface GrepOperations {
|
export interface GrepOperations {
|
||||||
/** Check if path is a directory. Throws if path doesn't exist. */
|
|
||||||
isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
|
isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
|
||||||
/** Read file contents for context lines */
|
|
||||||
readFile: (absolutePath: string) => Promise<string> | string;
|
readFile: (absolutePath: string) => Promise<string> | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultGrepOperations: GrepOperations = {
|
|
||||||
isDirectory: async p => (await Bun.file(p).stat()).isDirectory(),
|
|
||||||
readFile: p => Bun.file(p).text(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface GrepToolOptions {
|
export interface GrepToolOptions {
|
||||||
/** Custom operations for grep. Default: local filesystem + ripgrep */
|
|
||||||
operations?: GrepOperations;
|
operations?: GrepOperations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,46 +73,18 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|||||||
public readonly parameters = grepSchema;
|
public readonly parameters = grepSchema;
|
||||||
|
|
||||||
private readonly session: ToolSession;
|
private readonly session: ToolSession;
|
||||||
private readonly ops: GrepOperations;
|
|
||||||
|
|
||||||
constructor(session: ToolSession, options?: GrepToolOptions) {
|
constructor(session: ToolSession, _options?: GrepToolOptions) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.ops = options?.operations ?? defaultGrepOperations;
|
|
||||||
this.description = renderPromptTemplate(grepDescription);
|
this.description = renderPromptTemplate(grepDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a pattern against ripgrep's regex engine.
|
|
||||||
* Uses a quick dry-run against the null device to check for parse errors.
|
|
||||||
*/
|
|
||||||
private async validateRegexPattern(pattern: string, rgPath?: string): Promise<{ valid: boolean; error?: string }> {
|
|
||||||
if (!rgPath) {
|
|
||||||
return { valid: true }; // Can't validate, assume valid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run ripgrep against the null device with the pattern - this validates regex syntax
|
|
||||||
// without searching any files
|
|
||||||
const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
||||||
const result = await ptree.exec([rgPath, "--no-config", "--quiet", "--", pattern, nullDevice], {
|
|
||||||
allowNonZero: true,
|
|
||||||
allowAbort: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Exit code 1 = no matches (pattern is valid), 0 = matches found
|
|
||||||
// Exit code 2 = error (often regex parse error)
|
|
||||||
if (result.exitCode === 2 && result.stderr.includes("regex parse error")) {
|
|
||||||
return { valid: false, error: result.stderr.trim() };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async execute(
|
public async execute(
|
||||||
_toolCallId: string,
|
_toolCallId: string,
|
||||||
params: GrepParams,
|
params: GrepParams,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
||||||
toolContext?: AgentToolContext,
|
_toolContext?: AgentToolContext,
|
||||||
): Promise<AgentToolResult<GrepToolDetails>> {
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
||||||
const { pattern, path: searchDir, glob, type, output_mode, i, n, context, multiline, limit, offset } = params;
|
const { pattern, path: searchDir, glob, type, output_mode, i, n, context, multiline, limit, offset } = params;
|
||||||
|
|
||||||
@@ -189,38 +105,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|||||||
}
|
}
|
||||||
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
||||||
|
|
||||||
const normalizeContext = (value: number | undefined, label: string): number => {
|
const normalizedContext = context ?? 5;
|
||||||
if (value === undefined) return 0;
|
|
||||||
const normalized = Number.isFinite(value) ? Math.floor(value) : Number.NaN;
|
|
||||||
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
||||||
throw new ToolError(`${label} must be a non-negative number`);
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizedContext = normalizeContext(context ?? 5, "Context");
|
|
||||||
const showLineNumbers = n ?? true;
|
const showLineNumbers = n ?? true;
|
||||||
const ignoreCase = i ?? false;
|
const ignoreCase = i ?? false;
|
||||||
const normalizedGlob = glob?.trim() ?? "";
|
|
||||||
const normalizedType = type?.trim() ?? "";
|
|
||||||
const hasContentHints = limit !== undefined || context !== undefined;
|
const hasContentHints = limit !== undefined || context !== undefined;
|
||||||
|
|
||||||
// Validate regex patterns early to surface parse errors before running rg
|
|
||||||
const rgPath = await ensureTool("rg", {
|
|
||||||
silent: true,
|
|
||||||
notify: message => toolContext?.ui?.notify(message, "info"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!rgPath) {
|
|
||||||
throw new ToolError("rg is not available and could not be downloaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = await this.validateRegexPattern(normalizedPattern, rgPath);
|
|
||||||
if (!validation.valid) {
|
|
||||||
throw new ToolError(validation.error ?? "Invalid regex pattern");
|
|
||||||
}
|
|
||||||
|
|
||||||
// rgPath resolved earlier
|
|
||||||
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
||||||
const scopePath = (() => {
|
const scopePath = (() => {
|
||||||
const relative = nodePath.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
const relative = nodePath.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
||||||
@@ -229,98 +118,50 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|||||||
|
|
||||||
let isDirectory: boolean;
|
let isDirectory: boolean;
|
||||||
try {
|
try {
|
||||||
isDirectory = await this.ops.isDirectory(searchPath);
|
const stat = await Bun.file(searchPath).stat();
|
||||||
|
isDirectory = stat.isDirectory();
|
||||||
} catch {
|
} catch {
|
||||||
throw new ToolError(`Path not found: ${searchPath}`);
|
throw new ToolError(`Path not found: ${searchPath}`);
|
||||||
}
|
}
|
||||||
const effectiveOutputMode =
|
|
||||||
output_mode ?? (!isDirectory || hasContentHints ? "content" : "files_with_matches");
|
const effectiveOutputMode = output_mode ?? (!isDirectory || hasContentHints ? "content" : "filesWithMatches");
|
||||||
const effectiveOffset = normalizedOffset > 0 ? normalizedOffset : 0;
|
|
||||||
const effectiveLimit =
|
const effectiveLimit =
|
||||||
effectiveOutputMode === "content" ? (normalizedLimit ?? DEFAULT_MATCH_LIMIT) : normalizedLimit;
|
effectiveOutputMode === "content" ? (normalizedLimit ?? DEFAULT_MATCH_LIMIT) : normalizedLimit;
|
||||||
|
|
||||||
|
// Run WASM grep
|
||||||
|
let result: Awaited<ReturnType<typeof wasmGrep>>;
|
||||||
|
try {
|
||||||
|
result = await wasmGrep({
|
||||||
|
pattern: normalizedPattern,
|
||||||
|
path: searchPath,
|
||||||
|
glob: glob?.trim() || undefined,
|
||||||
|
type: type?.trim() || undefined,
|
||||||
|
ignoreCase,
|
||||||
|
multiline: multiline ?? false,
|
||||||
|
hidden: true,
|
||||||
|
maxCount: effectiveLimit,
|
||||||
|
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
||||||
|
context: effectiveOutputMode === "content" ? normalizedContext : undefined,
|
||||||
|
maxColumns: DEFAULT_MAX_COLUMN,
|
||||||
|
mode: effectiveOutputMode,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.message.startsWith("regex parse error")) {
|
||||||
|
throw new ToolError(err.message);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const formatPath = (filePath: string): string => {
|
const formatPath = (filePath: string): string => {
|
||||||
|
// WASM returns paths starting with / (the virtual root)
|
||||||
|
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
const relative = nodePath.relative(searchPath, filePath);
|
return cleanPath.replace(/\\/g, "/");
|
||||||
if (relative && !relative.startsWith("..")) {
|
|
||||||
return relative.replace(/\\/g, "/");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nodePath.basename(filePath);
|
return nodePath.basename(cleanPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileCache = new Map<string, Promise<string[]>>();
|
// Build output
|
||||||
const getFileLines = async (filePath: string): Promise<string[]> => {
|
|
||||||
let linesPromise = fileCache.get(filePath);
|
|
||||||
if (!linesPromise) {
|
|
||||||
linesPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const content = await this.ops.readFile(filePath);
|
|
||||||
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
fileCache.set(filePath, linesPromise);
|
|
||||||
}
|
|
||||||
return linesPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
const args: string[] = [];
|
|
||||||
|
|
||||||
// Ignore user config files for consistent behavior
|
|
||||||
args.push("--no-config");
|
|
||||||
|
|
||||||
// Base arguments depend on output mode
|
|
||||||
if (effectiveOutputMode === "files_with_matches") {
|
|
||||||
args.push("--files-with-matches", "--color=never");
|
|
||||||
} else if (effectiveOutputMode === "count") {
|
|
||||||
args.push("--count", "--color=never");
|
|
||||||
} else {
|
|
||||||
args.push("--json", "--color=never");
|
|
||||||
if (showLineNumbers) {
|
|
||||||
args.push("--line-number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("--hidden");
|
|
||||||
|
|
||||||
if (ignoreCase) {
|
|
||||||
args.push("--ignore-case");
|
|
||||||
} else {
|
|
||||||
args.push("--case-sensitive");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (multiline) {
|
|
||||||
args.push("--multiline");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedGlob) {
|
|
||||||
args.push("--glob", normalizedGlob);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("--glob", "!**/.git/**");
|
|
||||||
args.push("--glob", "!**/node_modules/**");
|
|
||||||
|
|
||||||
if (normalizedType) {
|
|
||||||
args.push("--type", normalizedType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effectiveOutputMode === "content") {
|
|
||||||
if (normalizedContext > 0) {
|
|
||||||
args.push("-C", String(normalizedContext));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("--", normalizedPattern, searchPath);
|
|
||||||
|
|
||||||
using child = ptree.spawn([rgPath, ...args], { signal });
|
|
||||||
|
|
||||||
let matchCount = 0;
|
|
||||||
let matchLimitReached = false;
|
|
||||||
let linesTruncated = false;
|
|
||||||
let killedDueToLimit = false;
|
|
||||||
const outputLines: string[] = [];
|
|
||||||
const files = new Set<string>();
|
const files = new Set<string>();
|
||||||
const fileList: string[] = [];
|
const fileList: string[] = [];
|
||||||
const fileMatchCounts = new Map<string, number>();
|
const fileMatchCounts = new Map<string, number>();
|
||||||
@@ -333,289 +174,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const recordFileMatch = (filePath: string) => {
|
if (result.totalMatches === 0) {
|
||||||
const relative = formatPath(filePath);
|
|
||||||
fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For simple output modes (files_with_matches, count), process text directly
|
|
||||||
if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
|
|
||||||
const stdout = await child.text().catch(x => {
|
|
||||||
if (x instanceof ptree.Exception && x.exitCode === 1) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return Promise.reject(x);
|
|
||||||
});
|
|
||||||
|
|
||||||
const exitCode = child.exitCode ?? 0;
|
|
||||||
if (exitCode !== 0 && exitCode !== 1) {
|
|
||||||
const errorMsg = child.peekStderr().trim() || `ripgrep exited with code ${exitCode}`;
|
|
||||||
throw new ToolError(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = stdout
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.filter(line => line.length > 0);
|
|
||||||
|
|
||||||
if (lines.length === 0) {
|
|
||||||
const details: GrepToolDetails = {
|
|
||||||
scopePath,
|
|
||||||
matchCount: 0,
|
|
||||||
fileCount: 0,
|
|
||||||
files: [],
|
|
||||||
mode: effectiveOutputMode,
|
|
||||||
truncated: false,
|
|
||||||
};
|
|
||||||
return toolResult(details).text("No matches found").done();
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetLines = effectiveOffset > 0 ? lines.slice(effectiveOffset) : lines;
|
|
||||||
const listLimit = applyListLimit(offsetLines, {
|
|
||||||
limit: normalizedLimit,
|
|
||||||
limitType: "result",
|
|
||||||
});
|
|
||||||
const processedLines = listLimit.items;
|
|
||||||
const limitMeta = listLimit.meta;
|
|
||||||
|
|
||||||
let simpleMatchCount = 0;
|
|
||||||
let fileCount = 0;
|
|
||||||
const simpleFiles = new Set<string>();
|
|
||||||
const simpleFileList: string[] = [];
|
|
||||||
const simpleFileMatchCounts = new Map<string, number>();
|
|
||||||
|
|
||||||
const recordSimpleFile = (filePath: string) => {
|
|
||||||
const relative = formatPath(filePath);
|
|
||||||
if (!simpleFiles.has(relative)) {
|
|
||||||
simpleFiles.add(relative);
|
|
||||||
simpleFileList.push(relative);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Count mode: ripgrep provides total count per file, so we set directly (not increment)
|
|
||||||
const setFileMatchCount = (filePath: string, count: number) => {
|
|
||||||
const relative = formatPath(filePath);
|
|
||||||
simpleFileMatchCounts.set(relative, count);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (effectiveOutputMode === "files_with_matches") {
|
|
||||||
for (const line of processedLines) {
|
|
||||||
recordSimpleFile(line);
|
|
||||||
}
|
|
||||||
fileCount = simpleFiles.size;
|
|
||||||
simpleMatchCount = fileCount;
|
|
||||||
} else {
|
|
||||||
for (const line of processedLines) {
|
|
||||||
const separatorIndex = line.lastIndexOf(":");
|
|
||||||
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
||||||
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
||||||
const count = Number.parseInt(countPart, 10);
|
|
||||||
recordSimpleFile(filePart);
|
|
||||||
if (!Number.isNaN(count)) {
|
|
||||||
simpleMatchCount += count;
|
|
||||||
setFileMatchCount(filePart, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileCount = simpleFiles.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
const truncatedByLimit = Boolean(limitMeta.resultLimit);
|
|
||||||
|
|
||||||
// For count mode, format as "path:count"
|
|
||||||
if (effectiveOutputMode === "count") {
|
|
||||||
const formatted = processedLines.map(line => {
|
|
||||||
const separatorIndex = line.lastIndexOf(":");
|
|
||||||
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
|
||||||
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
|
||||||
return `${relative}:${count}`;
|
|
||||||
});
|
|
||||||
const output = formatted.join("\n");
|
|
||||||
const details: GrepToolDetails = {
|
|
||||||
scopePath,
|
|
||||||
matchCount: simpleMatchCount,
|
|
||||||
fileCount,
|
|
||||||
files: simpleFileList,
|
|
||||||
fileMatches: simpleFileList.map(path => ({
|
|
||||||
path,
|
|
||||||
count: simpleFileMatchCounts.get(path) ?? 0,
|
|
||||||
})),
|
|
||||||
mode: effectiveOutputMode,
|
|
||||||
truncated: truncatedByLimit,
|
|
||||||
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
||||||
};
|
|
||||||
return toolResult(details)
|
|
||||||
.text(output)
|
|
||||||
.limits({
|
|
||||||
resultLimit: limitMeta.resultLimit?.reached,
|
|
||||||
})
|
|
||||||
.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For files_with_matches, format paths
|
|
||||||
const formatted = processedLines.map(line => formatPath(line));
|
|
||||||
const output = formatted.join("\n");
|
|
||||||
const details: GrepToolDetails = {
|
|
||||||
scopePath,
|
|
||||||
matchCount: simpleMatchCount,
|
|
||||||
fileCount,
|
|
||||||
files: simpleFileList,
|
|
||||||
mode: effectiveOutputMode,
|
|
||||||
truncated: truncatedByLimit,
|
|
||||||
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
||||||
};
|
|
||||||
return toolResult(details)
|
|
||||||
.text(output)
|
|
||||||
.limits({
|
|
||||||
resultLimit: limitMeta.resultLimit?.reached,
|
|
||||||
})
|
|
||||||
.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content mode - existing JSON processing
|
|
||||||
const formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {
|
|
||||||
const relativePath = formatPath(filePath);
|
|
||||||
const lines = await getFileLines(filePath);
|
|
||||||
if (!lines.length) {
|
|
||||||
return showLineNumbers
|
|
||||||
? [`${relativePath}:${lineNumber}: (unable to read file)`]
|
|
||||||
: [`${relativePath}: (unable to read file)`];
|
|
||||||
}
|
|
||||||
|
|
||||||
const block: string[] = [];
|
|
||||||
const start = normalizedContext > 0 ? Math.max(1, lineNumber - normalizedContext) : lineNumber;
|
|
||||||
const end = normalizedContext > 0 ? Math.min(lines.length, lineNumber + normalizedContext) : lineNumber;
|
|
||||||
|
|
||||||
for (let current = start; current <= end; current++) {
|
|
||||||
const lineText = lines[current - 1] ?? "";
|
|
||||||
const sanitized = lineText.replace(/\r/g, "");
|
|
||||||
const isMatchLine = current === lineNumber;
|
|
||||||
|
|
||||||
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
||||||
if (wasTruncated) {
|
|
||||||
linesTruncated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMatchLine) {
|
|
||||||
block.push(
|
|
||||||
showLineNumbers
|
|
||||||
? `${relativePath}:${current}: ${truncatedText}`
|
|
||||||
: `${relativePath}: ${truncatedText}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
block.push(
|
|
||||||
showLineNumbers
|
|
||||||
? `${relativePath}-${current}- ${truncatedText}`
|
|
||||||
: `${relativePath}- ${truncatedText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return block;
|
|
||||||
};
|
|
||||||
|
|
||||||
const maxMatches = effectiveLimit !== undefined ? effectiveLimit + effectiveOffset : undefined;
|
|
||||||
const processEvent = async (event: unknown): Promise<void> => {
|
|
||||||
if (!event || typeof event !== "object") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parsed = event as { type?: string; data?: { path?: { text?: string }; line_number?: number } };
|
|
||||||
if (parsed.type !== "match") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextIndex = matchCount + 1;
|
|
||||||
if (maxMatches !== undefined && nextIndex > maxMatches) {
|
|
||||||
matchLimitReached = true;
|
|
||||||
killedDueToLimit = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
matchCount = nextIndex;
|
|
||||||
const filePath = parsed.data?.path?.text;
|
|
||||||
const lineNumber = parsed.data?.line_number;
|
|
||||||
|
|
||||||
if (filePath && typeof lineNumber === "number") {
|
|
||||||
if (matchCount <= effectiveOffset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
recordFile(filePath);
|
|
||||||
recordFileMatch(filePath);
|
|
||||||
const block = await formatBlock(filePath, lineNumber);
|
|
||||||
outputLines.push(...block);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let buffer = "";
|
|
||||||
const parseBuffer = async () => {
|
|
||||||
while (buffer.length > 0) {
|
|
||||||
const result = Bun.JSONL.parseChunk(buffer);
|
|
||||||
for (const value of result.values) {
|
|
||||||
await processEvent(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.read > 0) {
|
|
||||||
buffer = buffer.slice(result.read);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
const nextNewline = buffer.indexOf("\n");
|
|
||||||
if (nextNewline === -1) {
|
|
||||||
buffer = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buffer = buffer.slice(nextNewline + 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.read === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process stdout stream with JSONL chunk parsing
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
try {
|
|
||||||
for await (const chunk of child.stdout) {
|
|
||||||
if (killedDueToLimit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buffer += decoder.decode(chunk, { stream: true });
|
|
||||||
await parseBuffer();
|
|
||||||
}
|
|
||||||
if (!killedDueToLimit) {
|
|
||||||
buffer += decoder.decode();
|
|
||||||
await parseBuffer();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ptree.Exception && err.aborted) {
|
|
||||||
throw new ToolAbortError();
|
|
||||||
}
|
|
||||||
// Stream may close early if we killed due to limit - that's ok
|
|
||||||
if (!killedDueToLimit) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for process to exit
|
|
||||||
try {
|
|
||||||
await child.exitedCleanly;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ptree.Exception) {
|
|
||||||
if (err.aborted) {
|
|
||||||
throw new ToolAbortError();
|
|
||||||
}
|
|
||||||
// Non-zero exit is ok if we killed due to limit or exit code 1 (no matches)
|
|
||||||
if (!killedDueToLimit && err.exitCode !== 1) {
|
|
||||||
const errorMsg = child.peekStderr().trim() || `ripgrep exited with code ${err.exitCode}`;
|
|
||||||
throw new ToolError(errorMsg);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchCount === 0) {
|
|
||||||
const details: GrepToolDetails = {
|
const details: GrepToolDetails = {
|
||||||
scopePath,
|
scopePath,
|
||||||
matchCount: 0,
|
matchCount: 0,
|
||||||
@@ -627,21 +186,76 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|||||||
return toolResult(details).text("No matches found").done();
|
return toolResult(details).text("No matches found").done();
|
||||||
}
|
}
|
||||||
|
|
||||||
const limitMeta =
|
let outputLines: string[] = [];
|
||||||
matchLimitReached && effectiveLimit !== undefined
|
let linesTruncated = false;
|
||||||
? { matchLimit: { reached: effectiveLimit, suggestion: effectiveLimit * 2 } }
|
|
||||||
: {};
|
for (const match of result.matches) {
|
||||||
|
recordFile(match.path);
|
||||||
|
const relativePath = formatPath(match.path);
|
||||||
|
|
||||||
|
if (effectiveOutputMode === "content") {
|
||||||
|
// Add context before
|
||||||
|
if (match.contextBefore) {
|
||||||
|
for (const ctx of match.contextBefore) {
|
||||||
|
outputLines.push(
|
||||||
|
showLineNumbers
|
||||||
|
? `${relativePath}-${ctx.lineNumber}- ${ctx.line}`
|
||||||
|
: `${relativePath}- ${ctx.line}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add match line
|
||||||
|
outputLines.push(
|
||||||
|
showLineNumbers
|
||||||
|
? `${relativePath}:${match.lineNumber}: ${match.line}`
|
||||||
|
: `${relativePath}: ${match.line}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match.truncated) {
|
||||||
|
linesTruncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add context after
|
||||||
|
if (match.contextAfter) {
|
||||||
|
for (const ctx of match.contextAfter) {
|
||||||
|
outputLines.push(
|
||||||
|
showLineNumbers
|
||||||
|
? `${relativePath}-${ctx.lineNumber}- ${ctx.line}`
|
||||||
|
: `${relativePath}- ${ctx.line}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track per-file counts
|
||||||
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
||||||
|
} else if (effectiveOutputMode === "filesWithMatches") {
|
||||||
|
// One line per file
|
||||||
|
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
||||||
|
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 1);
|
||||||
|
} else {
|
||||||
|
// count mode
|
||||||
|
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
||||||
|
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format output based on mode
|
||||||
|
if (effectiveOutputMode === "filesWithMatches") {
|
||||||
|
outputLines = fileList;
|
||||||
|
} else if (effectiveOutputMode === "count") {
|
||||||
|
outputLines = fileList.map(f => `${f}:${fileMatchCounts.get(f) ?? 0}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply byte truncation (no line limit since we already have match limit)
|
|
||||||
const rawOutput = outputLines.join("\n");
|
const rawOutput = outputLines.join("\n");
|
||||||
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
||||||
|
|
||||||
const output = truncation.content;
|
const output = truncation.content;
|
||||||
const truncated = Boolean(matchLimitReached || truncation.truncated || limitMeta.matchLimit || linesTruncated);
|
|
||||||
|
const truncated = Boolean(result.limitReached || truncation.truncated || linesTruncated);
|
||||||
const details: GrepToolDetails = {
|
const details: GrepToolDetails = {
|
||||||
scopePath,
|
scopePath,
|
||||||
matchCount: effectiveOffset > 0 ? Math.max(0, matchCount - effectiveOffset) : matchCount,
|
matchCount: result.totalMatches,
|
||||||
fileCount: files.size,
|
fileCount: result.filesWithMatches,
|
||||||
files: fileList,
|
files: fileList,
|
||||||
fileMatches: fileList.map(path => ({
|
fileMatches: fileList.map(path => ({
|
||||||
path,
|
path,
|
||||||
@@ -649,22 +263,19 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|||||||
})),
|
})),
|
||||||
mode: effectiveOutputMode,
|
mode: effectiveOutputMode,
|
||||||
truncated,
|
truncated,
|
||||||
matchLimitReached: limitMeta.matchLimit?.reached,
|
matchLimitReached: result.limitReached ? effectiveLimit : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep TUI compatibility fields
|
|
||||||
if (matchLimitReached && effectiveLimit !== undefined) {
|
|
||||||
details.matchLimitReached = effectiveLimit;
|
|
||||||
}
|
|
||||||
if (truncation.truncated) details.truncation = truncation;
|
if (truncation.truncated) details.truncation = truncation;
|
||||||
if (linesTruncated) details.linesTruncated = true;
|
if (linesTruncated) details.linesTruncated = true;
|
||||||
|
|
||||||
const resultBuilder = toolResult(details)
|
const resultBuilder = toolResult(details)
|
||||||
.text(output)
|
.text(output)
|
||||||
.limits({
|
.limits({
|
||||||
matchLimit: limitMeta.matchLimit?.reached,
|
matchLimit: result.limitReached ? effectiveLimit : undefined,
|
||||||
columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined,
|
columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (truncation.truncated) {
|
if (truncation.truncated) {
|
||||||
resultBuilder.truncation(truncation, { direction: "head" });
|
resultBuilder.truncation(truncation, { direction: "head" });
|
||||||
}
|
}
|
||||||
@@ -702,7 +313,7 @@ export const grepToolRenderer = {
|
|||||||
if (args.path) meta.push(`in ${args.path}`);
|
if (args.path) meta.push(`in ${args.path}`);
|
||||||
if (args.glob) meta.push(`glob:${args.glob}`);
|
if (args.glob) meta.push(`glob:${args.glob}`);
|
||||||
if (args.type) meta.push(`type:${args.type}`);
|
if (args.type) meta.push(`type:${args.type}`);
|
||||||
if (args.output_mode && args.output_mode !== "files_with_matches") meta.push(`mode:${args.output_mode}`);
|
if (args.output_mode && args.output_mode !== "filesWithMatches") meta.push(`mode:${args.output_mode}`);
|
||||||
if (args.i) meta.push("case:insensitive");
|
if (args.i) meta.push("case:insensitive");
|
||||||
if (args.n === false) meta.push("no-line-numbers");
|
if (args.n === false) meta.push("no-line-numbers");
|
||||||
if (args.context !== undefined && args.context > 0) meta.push(`context:${args.context}`);
|
if (args.context !== undefined && args.context > 0) meta.push(`context:${args.context}`);
|
||||||
@@ -758,7 +369,7 @@ export const grepToolRenderer = {
|
|||||||
|
|
||||||
const matchCount = details?.matchCount ?? 0;
|
const matchCount = details?.matchCount ?? 0;
|
||||||
const fileCount = details?.fileCount ?? 0;
|
const fileCount = details?.fileCount ?? 0;
|
||||||
const mode = details?.mode ?? "files_with_matches";
|
const mode = details?.mode ?? "filesWithMatches";
|
||||||
const truncation = details?.meta?.truncation;
|
const truncation = details?.meta?.truncation;
|
||||||
const limits = details?.meta?.limits;
|
const limits = details?.meta?.limits;
|
||||||
const truncated = Boolean(
|
const truncated = Boolean(
|
||||||
@@ -775,7 +386,7 @@ export const grepToolRenderer = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const summaryParts =
|
const summaryParts =
|
||||||
mode === "files_with_matches"
|
mode === "filesWithMatches"
|
||||||
? [formatCount("file", fileCount)]
|
? [formatCount("file", fileCount)]
|
||||||
: [formatCount("match", matchCount), formatCount("file", fileCount)];
|
: [formatCount("match", matchCount), formatCount("file", fileCount)];
|
||||||
const meta = [...summaryParts];
|
const meta = [...summaryParts];
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import * as os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||||
|
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
||||||
import type { Component } from "@oh-my-pi/pi-tui";
|
import type { Component } from "@oh-my-pi/pi-tui";
|
||||||
import { Text } from "@oh-my-pi/pi-tui";
|
import { Text } from "@oh-my-pi/pi-tui";
|
||||||
import { globPaths, ptree } from "@oh-my-pi/pi-utils";
|
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { CONFIG_DIR_NAME } from "../config";
|
import { CONFIG_DIR_NAME } from "../config";
|
||||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||||
@@ -165,27 +166,31 @@ async function listCandidateFiles(
|
|||||||
_notify?: (message: string) => void,
|
_notify?: (message: string) => void,
|
||||||
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
|
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
|
||||||
let files: string[];
|
let files: string[];
|
||||||
|
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
||||||
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
||||||
try {
|
try {
|
||||||
files = await globPaths("**/*", {
|
const result = await untilAborted(combinedSignal, () =>
|
||||||
cwd: searchRoot,
|
wasmFind({
|
||||||
gitignore: true,
|
pattern: "**/*",
|
||||||
dot: true,
|
path: searchRoot,
|
||||||
signal,
|
fileType: "file",
|
||||||
timeoutMs: GLOB_TIMEOUT_MS,
|
hidden: true,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
files = result.matches.map(match => match.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
if (timeoutSignal.aborted && !signal?.aborted) {
|
||||||
|
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
||||||
|
return { files: [], truncated: false, error: `find timed out after ${timeoutSeconds}s` };
|
||||||
|
}
|
||||||
throw new ToolAbortError();
|
throw new ToolAbortError();
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.name === "TimeoutError") {
|
|
||||||
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
|
||||||
return { files: [], truncated: false, error: `glob timed out after ${timeoutSeconds}s` };
|
|
||||||
}
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
return { files: [], truncated: false, error: message };
|
return { files: [], truncated: false, error: message };
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedFiles = files.map(line => line.replace(/\r$/, "").trim()).filter(line => line.length > 0);
|
const normalizedFiles = files.filter(line => line.length > 0);
|
||||||
const truncated = normalizedFiles.length > MAX_FUZZY_CANDIDATES;
|
const truncated = normalizedFiles.length > MAX_FUZZY_CANDIDATES;
|
||||||
const limited = truncated ? normalizedFiles.slice(0, MAX_FUZZY_CANDIDATES) : normalizedFiles;
|
const limited = truncated ? normalizedFiles.slice(0, MAX_FUZZY_CANDIDATES) : normalizedFiles;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as photon from "../vendor/photon";
|
import { PhotonImage } from "@oh-my-pi/pi-natives";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert image to PNG format for terminal display.
|
* Convert image to PNG format for terminal display.
|
||||||
@@ -14,16 +14,12 @@ export async function convertToPng(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64")));
|
using image = await PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64")));
|
||||||
try {
|
const pngBuffer = await image.get_bytes();
|
||||||
const pngBuffer = image.get_bytes();
|
return {
|
||||||
return {
|
data: Buffer.from(pngBuffer).toString("base64"),
|
||||||
data: Buffer.from(pngBuffer).toString("base64"),
|
mimeType: "image/png",
|
||||||
mimeType: "image/png",
|
};
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
image.free();
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Conversion failed
|
// Conversion failed
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||||
import * as photon from "../vendor/photon";
|
import { PhotonImage, resize, SamplingFilter } from "@oh-my-pi/pi-natives";
|
||||||
|
|
||||||
export interface ImageResizeOptions {
|
export interface ImageResizeOptions {
|
||||||
maxWidth?: number; // Default: 2000
|
maxWidth?: number; // Default: 2000
|
||||||
@@ -53,9 +53,8 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||||
const inputBuffer = Buffer.from(img.data, "base64");
|
const inputBuffer = Buffer.from(img.data, "base64");
|
||||||
|
|
||||||
let image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;
|
|
||||||
try {
|
try {
|
||||||
image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
using image = await PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
||||||
|
|
||||||
const originalWidth = image.get_width();
|
const originalWidth = image.get_width();
|
||||||
const originalHeight = image.get_height();
|
const originalHeight = image.get_height();
|
||||||
@@ -89,24 +88,19 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to resize and encode in both formats, returning the smaller one
|
// Helper to resize and encode in both formats, returning the smaller one
|
||||||
function tryBothFormats(
|
async function tryBothFormats(
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
jpegQuality: number,
|
jpegQuality: number,
|
||||||
): { buffer: Uint8Array; mimeType: string } {
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
||||||
const resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);
|
using resized = await resize(image!, width, height, SamplingFilter.Lanczos3);
|
||||||
|
|
||||||
try {
|
const [pngBuffer, jpegBuffer] = await Promise.all([resized.get_bytes(), resized.get_bytes_jpeg(jpegQuality)]);
|
||||||
const pngBuffer = resized.get_bytes();
|
|
||||||
const jpegBuffer = resized.get_bytes_jpeg(jpegQuality);
|
|
||||||
|
|
||||||
return pickSmaller(
|
return pickSmaller(
|
||||||
{ buffer: pngBuffer, mimeType: "image/png" },
|
{ buffer: pngBuffer, mimeType: "image/png" },
|
||||||
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
resized.free();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to produce an image under maxBytes
|
// Try to produce an image under maxBytes
|
||||||
@@ -118,7 +112,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
let finalHeight = targetHeight;
|
let finalHeight = targetHeight;
|
||||||
|
|
||||||
// First attempt: resize to target dimensions, try both formats
|
// First attempt: resize to target dimensions, try both formats
|
||||||
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
||||||
|
|
||||||
if (best.buffer.length <= opts.maxBytes) {
|
if (best.buffer.length <= opts.maxBytes) {
|
||||||
return {
|
return {
|
||||||
@@ -134,7 +128,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
|
|
||||||
// Still too large - try JPEG with decreasing quality
|
// Still too large - try JPEG with decreasing quality
|
||||||
for (const quality of qualitySteps) {
|
for (const quality of qualitySteps) {
|
||||||
best = tryBothFormats(targetWidth, targetHeight, quality);
|
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
||||||
|
|
||||||
if (best.buffer.length <= opts.maxBytes) {
|
if (best.buffer.length <= opts.maxBytes) {
|
||||||
return {
|
return {
|
||||||
@@ -159,7 +153,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const quality of qualitySteps) {
|
for (const quality of qualitySteps) {
|
||||||
best = tryBothFormats(finalWidth, finalHeight, quality);
|
best = await tryBothFormats(finalWidth, finalHeight, quality);
|
||||||
|
|
||||||
if (best.buffer.length <= opts.maxBytes) {
|
if (best.buffer.length <= opts.maxBytes) {
|
||||||
return {
|
return {
|
||||||
@@ -196,10 +190,6 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
height: 0,
|
height: 0,
|
||||||
wasResized: false,
|
wasResized: false,
|
||||||
};
|
};
|
||||||
} finally {
|
|
||||||
if (image) {
|
|
||||||
image.free();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import * as os from "node:os";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { logger, TempDir } from "@oh-my-pi/pi-utils";
|
import { logger, TempDir } from "@oh-my-pi/pi-utils";
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import { APP_NAME, getBinDir } from "../config";
|
import { APP_NAME, getToolsDir } from "../config";
|
||||||
|
|
||||||
const TOOLS_DIR = getBinDir();
|
const TOOLS_DIR = getToolsDir();
|
||||||
const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
|
const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
interface ToolConfig {
|
interface ToolConfig {
|
||||||
@@ -18,46 +18,6 @@ interface ToolConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TOOLS: Record<string, ToolConfig> = {
|
const TOOLS: Record<string, ToolConfig> = {
|
||||||
fd: {
|
|
||||||
name: "fd",
|
|
||||||
repo: "sharkdp/fd",
|
|
||||||
binaryName: "fd",
|
|
||||||
tagPrefix: "v",
|
|
||||||
getAssetName: (version, plat, architecture) => {
|
|
||||||
if (plat === "darwin") {
|
|
||||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
||||||
return `fd-v${version}-${archStr}-apple-darwin.tar.gz`;
|
|
||||||
} else if (plat === "linux") {
|
|
||||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
||||||
return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;
|
|
||||||
} else if (plat === "win32") {
|
|
||||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
||||||
return `fd-v${version}-${archStr}-pc-windows-msvc.zip`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rg: {
|
|
||||||
name: "ripgrep",
|
|
||||||
repo: "BurntSushi/ripgrep",
|
|
||||||
binaryName: "rg",
|
|
||||||
tagPrefix: "",
|
|
||||||
getAssetName: (version, plat, architecture) => {
|
|
||||||
if (plat === "darwin") {
|
|
||||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
||||||
return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;
|
|
||||||
} else if (plat === "linux") {
|
|
||||||
if (architecture === "arm64") {
|
|
||||||
return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;
|
|
||||||
}
|
|
||||||
return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
|
|
||||||
} else if (plat === "win32") {
|
|
||||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
||||||
return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sd: {
|
sd: {
|
||||||
name: "sd",
|
name: "sd",
|
||||||
repo: "chmln/sd",
|
repo: "chmln/sd",
|
||||||
@@ -135,7 +95,7 @@ const PYTHON_TOOLS: Record<string, PythonToolConfig> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolName = "fd" | "rg" | "sd" | "sg" | "yt-dlp" | "markitdown" | "html2text";
|
export type ToolName = "sd" | "sg" | "yt-dlp" | "markitdown" | "html2text";
|
||||||
|
|
||||||
// Get the path to a tool (system-wide or in our tools dir)
|
// Get the path to a tool (system-wide or in our tools dir)
|
||||||
export async function getToolPath(tool: ToolName): Promise<string | null> {
|
export async function getToolPath(tool: ToolName): Promise<string | null> {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { abortableSleep, once, untilAborted } from "@oh-my-pi/pi-utils";
|
|
||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2023, Silvia O'Dwyer
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-158
@@ -1,158 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="" rel="noopener">
|
|
||||||
<img src="https://i.imgur.com/GxrKNOb.png" alt="Photon banner, showing the Photon logo on a dark background"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[]()
|
|
||||||
[](https://github.com/silvia-odwyer/photon/issues)
|
|
||||||
[](https://github.com/silvia-odwyer/p/pulls)
|
|
||||||
[](https://gitter.im/photonlibrary/community "Gitter chat")
|
|
||||||
[](https://crates.io/crates/photon-rs)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center"> High-performance, cross-platform Rust/WebAssembly image processing library
|
|
||||||
<br>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 📝 Table of Contents
|
|
||||||
- [Get Started with WebAssembly](https://github.com/silvia-odwyer/photon#-get-started-with-webassembly)
|
|
||||||
- [Get Started Natively](https://github.com/silvia-odwyer/photon#getting-started)
|
|
||||||
- [Documentation](https://docs.rs/photon-rs/0.1.0/)
|
|
||||||
- [Official Website.](https://silvia-odwyer.github.io/photon/)
|
|
||||||
- [All Available Functions.](https://silvia-odwyer.github.io/photon/docs/photon/all.html)
|
|
||||||
- [Got Questions? Ask Here!](https://github.com/silvia-odwyer/photon#got-questions)
|
|
||||||
|
|
||||||
Photon is a high-performance Rust image processing library, which compiles to WebAssembly, allowing for
|
|
||||||
safe, blazing-fast image processing both natively and on the web.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- **Fast:** Photon outperforms even the fastest of libraries, including ImageMagick. On the web, its high-performance allows for near-native-speed image processing on the web. Benchmarks coming soon.
|
|
||||||
- **Call with JS:** Want to use Photon on the web or with Node? Using a simple npm package, you're good to go. Get all the benefits of WebAssembly
|
|
||||||
with zero-cost abstraction.
|
|
||||||
- **Use Natively:** For command-line apps, native photo editing apps, and so forth, Photon's core codebase is in Rust, allowing for cross-platform
|
|
||||||
development.
|
|
||||||
|
|
||||||
### Live Demo
|
|
||||||
View the [official demo of WASM in action](https://silvia-odwyer.github.io/photon/demo.html).
|
|
||||||
|
|
||||||
### Get Started
|
|
||||||
#### Getting Started Guide
|
|
||||||
Check out Photon's [getting started guide, complete with tutorials, installation instructions, and more](https://silvia-odwyer.github.io/photon/guide)
|
|
||||||
|
|
||||||
#### Documentation
|
|
||||||
### 📚 Documentation
|
|
||||||
View the [official documentation](https://docs.rs/photon-rs/).
|
|
||||||
|
|
||||||
### Photon In Action
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Functionality
|
|
||||||
96+ customisable functions are available.
|
|
||||||
|
|
||||||
Resize, transform, correct, and filter images: apply as many effects as desired.
|
|
||||||
|
|
||||||
Functions include:
|
|
||||||
- **Image correction**: Hue rotation, sharpening, brightness adjustment, adjusting saturation, lightening/darkening all within various colour spaces.
|
|
||||||
- **Convolutions**: Sobel filters, blurs, Laplace effects, edge detection, etc.,
|
|
||||||
- **Channel manipulation**: Increasing/decreasing RGB channel values, swapping channels, removing channels, etc.
|
|
||||||
- **Transform**: Resize, crop, rotate and flip images.
|
|
||||||
- **Monochrome effects**: Duotoning, greyscaling of various forms, thresholding, sepia, averaging RGB values
|
|
||||||
- **Colour manipulation**: Work with the image in various colour spaces such as HSL, LCh, and sRGB, and adjust the colours accordingly.
|
|
||||||
- **Filters**: Over 30 pre-set filters available, incorporating various effects and transformations.
|
|
||||||
- **Text**: Apply text to imagery in artistic ways, or to watermark, etc.,
|
|
||||||
- **Watermarking**: Watermark images in multiple formats.
|
|
||||||
- **Blending**: Blend images together using 10 different techniques, change image backgrounds.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
### Native
|
|
||||||
Add the following line to the dependencies section of your Rust project's Cargo.toml:
|
|
||||||
|
|
||||||
###### Cargo.toml
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
photon-rs = "0.3.2"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web
|
|
||||||
Install Photon as an npm module:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install --save @silvia-odwyer/photon
|
|
||||||
```
|
|
||||||
|
|
||||||
### Node.JS
|
|
||||||
To install Photon as an npm module for Node.JS use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install --save @silvia-odwyer/photon-node
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using Photon Natively
|
|
||||||
The following code opens an image from the filesystem, applies an effect, and saves it.
|
|
||||||
|
|
||||||
Here is a code sample to get you started:
|
|
||||||
|
|
||||||
```rs
|
|
||||||
extern crate photon_rs;
|
|
||||||
use photon_rs::native::{open_image, save_image};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Open the image (a PhotonImage is returned)
|
|
||||||
let mut img = open_image("test_image.png");
|
|
||||||
|
|
||||||
// Increment the red channel by 40
|
|
||||||
photon_rs::channels::alter_red_channel(&mut img, 40);
|
|
||||||
|
|
||||||
// Write file to filesystem.
|
|
||||||
save_image(img, "raw_image.jpg");
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[For more examples, check out the guide on how to get started with Photon natively.](https://silvia-odwyer.github.io/photon/guide/using-photon-natively/)
|
|
||||||
|
|
||||||
#### Using Photon On The Web
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
Photon contains a series of modules, which include:
|
|
||||||
|
|
||||||
- `effects`: Various image effects, including adding offsets, thresholding, duotoning, solarization, etc.,
|
|
||||||
- `channels`: Functions related to increasing/decreasing the red, green, and blue channels of the image data.
|
|
||||||
- `transform`: Resize, crop, flip, and rotate images.
|
|
||||||
- `filters`: Preset filters, which alter the RGB channels of the image. Contains over 20.
|
|
||||||
- `conv`: Laplace, Sobel, emboss; image proc functions which require image convolution.
|
|
||||||
- `noise`: Noise generation of varying tints and hues.
|
|
||||||
- `multiple`: A module for dealing with multiple images, such as watermarking images, etc.,
|
|
||||||
- `correction`: Hue rotation, adjusting saturation, lightening/darkening: all techniques available in multiple colour spaces, which lead to varying effects.
|
|
||||||
|
|
||||||
All effects and filters can be viewed below and on the official website.
|
|
||||||
|
|
||||||
### Run Examples
|
|
||||||
Clone this crate's official GitHub repo:
|
|
||||||
```sh
|
|
||||||
git clone https://github.com/silvia-odwyer/photon
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the binary, which will perform an image processing function on an image:
|
|
||||||
```sh
|
|
||||||
cd crate
|
|
||||||
cargo run --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Compare the original image with the outputted image, and you'll see the desired effect has been applied.
|
|
||||||
|
|
||||||
### Got Questions?
|
|
||||||
If you'd like to chat to the developer about your potential use case, or have further questions about this library,
|
|
||||||
just submit them here, and I'll get back to you!
|
|
||||||
|
|
||||||
- [Spectrum Chat](https://spectrum.chat/photonlibrary)
|
|
||||||
- [Gitter](https://gitter.im/photonlibrary/community)
|
|
||||||
|
|
||||||
## License
|
|
||||||
This project is licensed under the Apache 2.0 License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
||||||
-3013
File diff suppressed because it is too large
Load Diff
-4521
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e51491958652d0c0523cc2a974913d57aaf8bed537c0d244c01df32f9928b6f6
|
|
||||||
size 1539011
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
export const memory: WebAssembly.Memory;
|
|
||||||
export const adjust_brightness: (a: number, b: number) => void;
|
|
||||||
export const adjust_contrast: (a: number, b: number) => void;
|
|
||||||
export const color_horizontal_strips: (a: number, b: number, c: number) => void;
|
|
||||||
export const color_vertical_strips: (a: number, b: number, c: number) => void;
|
|
||||||
export const colorize: (a: number) => void;
|
|
||||||
export const dec_brightness: (a: number, b: number) => void;
|
|
||||||
export const dither: (a: number, b: number) => void;
|
|
||||||
export const duotone: (a: number, b: number, c: number) => void;
|
|
||||||
export const frosted_glass: (a: number) => void;
|
|
||||||
export const halftone: (a: number) => void;
|
|
||||||
export const horizontal_strips: (a: number, b: number) => void;
|
|
||||||
export const inc_brightness: (a: number, b: number) => void;
|
|
||||||
export const multiple_offsets: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const normalize: (a: number) => void;
|
|
||||||
export const offset: (a: number, b: number, c: number) => void;
|
|
||||||
export const offset_blue: (a: number, b: number) => void;
|
|
||||||
export const offset_green: (a: number, b: number) => void;
|
|
||||||
export const offset_red: (a: number, b: number) => void;
|
|
||||||
export const oil: (a: number, b: number, c: number) => void;
|
|
||||||
export const pixelize: (a: number, b: number) => void;
|
|
||||||
export const primary: (a: number) => void;
|
|
||||||
export const solarize: (a: number) => void;
|
|
||||||
export const solarize_retimg: (a: number) => number;
|
|
||||||
export const tint: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const vertical_strips: (a: number, b: number) => void;
|
|
||||||
export const apply_gradient: (a: number) => void;
|
|
||||||
export const blend: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const create_gradient: (a: number, b: number) => number;
|
|
||||||
export const watermark: (a: number, b: number, c: bigint, d: bigint) => void;
|
|
||||||
export const b_grayscale: (a: number) => void;
|
|
||||||
export const darken_hsl: (a: number, b: number) => void;
|
|
||||||
export const darken_hsluv: (a: number, b: number) => void;
|
|
||||||
export const darken_hsv: (a: number, b: number) => void;
|
|
||||||
export const darken_lch: (a: number, b: number) => void;
|
|
||||||
export const decompose_max: (a: number) => void;
|
|
||||||
export const decompose_min: (a: number) => void;
|
|
||||||
export const desaturate: (a: number) => void;
|
|
||||||
export const desaturate_hsl: (a: number, b: number) => void;
|
|
||||||
export const desaturate_hsluv: (a: number, b: number) => void;
|
|
||||||
export const desaturate_hsv: (a: number, b: number) => void;
|
|
||||||
export const desaturate_lch: (a: number, b: number) => void;
|
|
||||||
export const draw_text: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
|
||||||
export const draw_text_with_border: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
|
||||||
export const g_grayscale: (a: number) => void;
|
|
||||||
export const gamma_correction: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const grayscale: (a: number) => void;
|
|
||||||
export const grayscale_human_corrected: (a: number) => void;
|
|
||||||
export const grayscale_shades: (a: number, b: number) => void;
|
|
||||||
export const hsl: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const hsluv: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const hsv: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const hue_rotate_hsl: (a: number, b: number) => void;
|
|
||||||
export const hue_rotate_hsluv: (a: number, b: number) => void;
|
|
||||||
export const hue_rotate_hsv: (a: number, b: number) => void;
|
|
||||||
export const hue_rotate_lch: (a: number, b: number) => void;
|
|
||||||
export const lch: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const lighten_hsl: (a: number, b: number) => void;
|
|
||||||
export const lighten_hsluv: (a: number, b: number) => void;
|
|
||||||
export const lighten_hsv: (a: number, b: number) => void;
|
|
||||||
export const lighten_lch: (a: number, b: number) => void;
|
|
||||||
export const mix_with_colour: (a: number, b: number, c: number) => void;
|
|
||||||
export const monochrome: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const r_grayscale: (a: number) => void;
|
|
||||||
export const saturate_hsl: (a: number, b: number) => void;
|
|
||||||
export const saturate_hsluv: (a: number, b: number) => void;
|
|
||||||
export const saturate_hsv: (a: number, b: number) => void;
|
|
||||||
export const saturate_lch: (a: number, b: number) => void;
|
|
||||||
export const sepia: (a: number) => void;
|
|
||||||
export const single_channel_grayscale: (a: number, b: number) => void;
|
|
||||||
export const threshold: (a: number, b: number) => void;
|
|
||||||
export const crop: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
||||||
export const crop_img_browser: (a: any, b: number, c: number, d: number, e: number) => any;
|
|
||||||
export const fliph: (a: number) => void;
|
|
||||||
export const flipv: (a: number) => void;
|
|
||||||
export const padding_bottom: (a: number, b: number, c: number) => number;
|
|
||||||
export const padding_left: (a: number, b: number, c: number) => number;
|
|
||||||
export const padding_right: (a: number, b: number, c: number) => number;
|
|
||||||
export const padding_top: (a: number, b: number, c: number) => number;
|
|
||||||
export const padding_uniform: (a: number, b: number, c: number) => number;
|
|
||||||
export const resample: (a: number, b: number, c: number) => number;
|
|
||||||
export const resize: (a: number, b: number, c: number, d: number) => number;
|
|
||||||
export const resize_img_browser: (a: number, b: number, c: number, d: number) => any;
|
|
||||||
export const rotate: (a: number, b: number) => number;
|
|
||||||
export const seam_carve: (a: number, b: number, c: number) => number;
|
|
||||||
export const shearx: (a: number, b: number) => number;
|
|
||||||
export const sheary: (a: number, b: number) => number;
|
|
||||||
export const __wbg_photonimage_free: (a: number, b: number) => void;
|
|
||||||
export const __wbg_rgb_free: (a: number, b: number) => void;
|
|
||||||
export const __wbg_rgba_free: (a: number, b: number) => void;
|
|
||||||
export const base64_to_image: (a: number, b: number) => number;
|
|
||||||
export const base64_to_vec: (a: number, b: number) => [number, number];
|
|
||||||
export const get_image_data: (a: any, b: any) => any;
|
|
||||||
export const open_image: (a: any, b: any) => number;
|
|
||||||
export const photonimage_get_base64: (a: number) => [number, number];
|
|
||||||
export const photonimage_get_bytes: (a: number) => [number, number];
|
|
||||||
export const photonimage_get_bytes_jpeg: (a: number, b: number) => [number, number];
|
|
||||||
export const photonimage_get_bytes_webp: (a: number) => [number, number];
|
|
||||||
export const photonimage_get_estimated_filesize: (a: number) => bigint;
|
|
||||||
export const photonimage_get_height: (a: number) => number;
|
|
||||||
export const photonimage_get_image_data: (a: number) => any;
|
|
||||||
export const photonimage_get_raw_pixels: (a: number) => [number, number];
|
|
||||||
export const photonimage_get_width: (a: number) => number;
|
|
||||||
export const photonimage_new: (a: number, b: number, c: number, d: number) => number;
|
|
||||||
export const photonimage_new_from_blob: (a: any) => number;
|
|
||||||
export const photonimage_new_from_byteslice: (a: number, b: number) => number;
|
|
||||||
export const photonimage_new_from_image: (a: any) => number;
|
|
||||||
export const photonimage_set_imgdata: (a: number, b: any) => void;
|
|
||||||
export const putImageData: (a: any, b: any, c: number) => void;
|
|
||||||
export const rgb_get_blue: (a: number) => number;
|
|
||||||
export const rgb_get_green: (a: number) => number;
|
|
||||||
export const rgb_get_red: (a: number) => number;
|
|
||||||
export const rgb_new: (a: number, b: number, c: number) => number;
|
|
||||||
export const rgb_set_blue: (a: number, b: number) => void;
|
|
||||||
export const rgb_set_green: (a: number, b: number) => void;
|
|
||||||
export const rgb_set_red: (a: number, b: number) => void;
|
|
||||||
export const rgba_get_alpha: (a: number) => number;
|
|
||||||
export const rgba_new: (a: number, b: number, c: number, d: number) => number;
|
|
||||||
export const rgba_set_alpha: (a: number, b: number) => void;
|
|
||||||
export const run: () => [number, number];
|
|
||||||
export const to_image_data: (a: number) => any;
|
|
||||||
export const to_raw_pixels: (a: any) => [number, number];
|
|
||||||
export const rgba_get_blue: (a: number) => number;
|
|
||||||
export const rgba_get_green: (a: number) => number;
|
|
||||||
export const rgba_get_red: (a: number) => number;
|
|
||||||
export const photonimage_new_from_base64: (a: number, b: number) => number;
|
|
||||||
export const rgba_set_blue: (a: number, b: number) => void;
|
|
||||||
export const rgba_set_green: (a: number, b: number) => void;
|
|
||||||
export const rgba_set_red: (a: number, b: number) => void;
|
|
||||||
export const box_blur: (a: number) => void;
|
|
||||||
export const detect_135_deg_lines: (a: number) => void;
|
|
||||||
export const detect_45_deg_lines: (a: number) => void;
|
|
||||||
export const detect_horizontal_lines: (a: number) => void;
|
|
||||||
export const detect_vertical_lines: (a: number) => void;
|
|
||||||
export const edge_detection: (a: number) => void;
|
|
||||||
export const edge_one: (a: number) => void;
|
|
||||||
export const emboss: (a: number) => void;
|
|
||||||
export const gaussian_blur: (a: number, b: number) => void;
|
|
||||||
export const identity: (a: number) => void;
|
|
||||||
export const laplace: (a: number) => void;
|
|
||||||
export const noise_reduction: (a: number) => void;
|
|
||||||
export const prewitt_horizontal: (a: number) => void;
|
|
||||||
export const sharpen: (a: number) => void;
|
|
||||||
export const sobel_global: (a: number) => void;
|
|
||||||
export const sobel_horizontal: (a: number) => void;
|
|
||||||
export const sobel_vertical: (a: number) => void;
|
|
||||||
export const add_noise_rand: (a: number) => void;
|
|
||||||
export const alter_blue_channel: (a: number, b: number) => void;
|
|
||||||
export const alter_channel: (a: number, b: number, c: number) => void;
|
|
||||||
export const alter_channels: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const alter_green_channel: (a: number, b: number) => void;
|
|
||||||
export const alter_red_channel: (a: number, b: number) => void;
|
|
||||||
export const alter_two_channels: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
||||||
export const cali: (a: number) => void;
|
|
||||||
export const dramatic: (a: number) => void;
|
|
||||||
export const duotone_horizon: (a: number) => void;
|
|
||||||
export const duotone_lilac: (a: number) => void;
|
|
||||||
export const duotone_ochre: (a: number) => void;
|
|
||||||
export const duotone_tint: (a: number, b: number) => void;
|
|
||||||
export const duotone_violette: (a: number) => void;
|
|
||||||
export const filter: (a: number, b: number, c: number) => void;
|
|
||||||
export const firenze: (a: number) => void;
|
|
||||||
export const golden: (a: number) => void;
|
|
||||||
export const invert: (a: number) => void;
|
|
||||||
export const lix: (a: number) => void;
|
|
||||||
export const lofi: (a: number) => void;
|
|
||||||
export const monochrome_tint: (a: number, b: number) => void;
|
|
||||||
export const neue: (a: number) => void;
|
|
||||||
export const obsidian: (a: number) => void;
|
|
||||||
export const pastel_pink: (a: number) => void;
|
|
||||||
export const pink_noise: (a: number) => void;
|
|
||||||
export const remove_blue_channel: (a: number, b: number) => void;
|
|
||||||
export const remove_channel: (a: number, b: number, c: number) => void;
|
|
||||||
export const remove_green_channel: (a: number, b: number) => void;
|
|
||||||
export const remove_red_channel: (a: number, b: number) => void;
|
|
||||||
export const ryo: (a: number) => void;
|
|
||||||
export const selective_color_convert: (a: number, b: number, c: number, d: number) => void;
|
|
||||||
export const selective_desaturate: (a: number, b: number, c: number) => void;
|
|
||||||
export const selective_greyscale: (a: number, b: number) => void;
|
|
||||||
export const selective_hue_rotate: (a: number, b: number, c: number) => void;
|
|
||||||
export const selective_lighten: (a: number, b: number, c: number) => void;
|
|
||||||
export const selective_saturate: (a: number, b: number, c: number) => void;
|
|
||||||
export const swap_channels: (a: number, b: number, c: number) => void;
|
|
||||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
||||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
||||||
export const __wbindgen_exn_store: (a: number) => void;
|
|
||||||
export const __externref_table_alloc: () => number;
|
|
||||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
|
||||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
||||||
export const __externref_table_dealloc: (a: number) => void;
|
|
||||||
export const __wbindgen_start: () => void;
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
import { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
||||||
import { YAML } from "bun";
|
import { YAML } from "bun";
|
||||||
@@ -11,7 +12,7 @@ describe("SettingsManager", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Use random UUID to isolate parallel test runs (SQLite files can't be shared)
|
// Use random UUID to isolate parallel test runs (SQLite files can't be shared)
|
||||||
testDir = path.join(process.cwd(), "test-settings-tmp", crypto.randomUUID());
|
testDir = path.join(os.tmpdir(), "test-settings-tmp", crypto.randomUUID());
|
||||||
agentDir = path.join(testDir, "agent");
|
agentDir = path.join(testDir, "agent");
|
||||||
projectDir = path.join(testDir, "project");
|
projectDir = path.join(testDir, "project");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# @oh-my-pi/pi-natives
|
||||||
|
|
||||||
|
Native Rust functionality compiled to WebAssembly via wasm-bindgen.
|
||||||
|
|
||||||
|
## What's Inside
|
||||||
|
|
||||||
|
- **Grep**: Regex-based search powered by ripgrep's engine (WASM handles matching, JS handles I/O + gitignore-aware file walking)
|
||||||
|
- **Find**: Glob-based file/directory discovery with gitignore support (pure TypeScript via `globPaths`)
|
||||||
|
- **Image**: Image processing via photon-rs (resize, format conversion)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { grep, find, PhotonImage, resize, SamplingFilter } from "@oh-my-pi/pi-natives";
|
||||||
|
|
||||||
|
// Grep for a pattern
|
||||||
|
const results = await grep({
|
||||||
|
pattern: "TODO",
|
||||||
|
path: "/path/to/project",
|
||||||
|
glob: "*.ts",
|
||||||
|
context: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find files
|
||||||
|
const files = await find({
|
||||||
|
pattern: "*.rs",
|
||||||
|
path: "/path/to/project",
|
||||||
|
fileType: "file",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Image processing
|
||||||
|
using image = await PhotonImage.new_from_byteslice(bytes);
|
||||||
|
using resized = await resize(image, 800, 600, SamplingFilter.Lanczos3);
|
||||||
|
const pngBytes = await resized.get_bytes();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build WASM from workspace root (requires Rust + wasm-pack)
|
||||||
|
bun run build:wasm
|
||||||
|
|
||||||
|
# Type check
|
||||||
|
bun run check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/pi-natives/ # Rust source (workspace member)
|
||||||
|
src/lib.rs # Grep/search + wasm-bindgen bindings
|
||||||
|
src/image.rs # Image processing (photon-rs)
|
||||||
|
Cargo.toml # Rust dependencies
|
||||||
|
wasm/ # Generated WASM output
|
||||||
|
pi_natives.wasm # Compiled WASM module
|
||||||
|
pi_natives.js # wasm-bindgen generated JS glue
|
||||||
|
pi_natives.d.ts # TypeScript definitions
|
||||||
|
src/ # TypeScript wrappers
|
||||||
|
index.ts # Public API
|
||||||
|
grep/ # Grep with worker pool
|
||||||
|
image/ # Async image processing via worker
|
||||||
|
pool.ts # Generic worker pool infrastructure
|
||||||
|
```
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { grep, grepPool, terminate } from "../src/index.js";
|
||||||
|
|
||||||
|
const ITERATIONS = 50;
|
||||||
|
const CONCURRENCY = 8;
|
||||||
|
|
||||||
|
const packages = path.resolve(import.meta.dir, "../..");
|
||||||
|
|
||||||
|
interface BenchCase {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
pattern: string;
|
||||||
|
glob?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases: BenchCase[] = [
|
||||||
|
{ name: "Medium (50 files)", path: path.resolve(packages, "tui/src"), pattern: "export", glob: "*.ts" },
|
||||||
|
{ name: "Large (200+ files)", path: path.resolve(packages, "coding-agent/src"), pattern: "import", glob: "*.ts" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Warmup
|
||||||
|
await grep({ pattern: "test", path: path.resolve(packages, "tui/src") });
|
||||||
|
await grepPool({ pattern: "test", path: path.resolve(packages, "tui/src") });
|
||||||
|
|
||||||
|
console.log(`Benchmark: ${ITERATIONS} iterations per case\n`);
|
||||||
|
|
||||||
|
for (const c of cases) {
|
||||||
|
// Main thread sequential
|
||||||
|
const mainTimes: number[] = [];
|
||||||
|
let mainMatches = 0;
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = await grep({ pattern: c.pattern, path: c.path, glob: c.glob });
|
||||||
|
mainTimes.push(performance.now() - start);
|
||||||
|
mainMatches = result.totalMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main thread concurrent (8x parallel)
|
||||||
|
const mainConcurrentTimes: number[] = [];
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
const start = performance.now();
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: CONCURRENCY }, () => grep({ pattern: c.pattern, path: c.path, glob: c.glob })),
|
||||||
|
);
|
||||||
|
mainConcurrentTimes.push(performance.now() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker pool sequential
|
||||||
|
const poolTimes: number[] = [];
|
||||||
|
let poolMatches = 0;
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = await grepPool({ pattern: c.pattern, path: c.path, glob: c.glob });
|
||||||
|
poolTimes.push(performance.now() - start);
|
||||||
|
poolMatches = result.totalMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker pool concurrent (8x parallel)
|
||||||
|
const poolConcurrentTimes: number[] = [];
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
const start = performance.now();
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: CONCURRENCY }, () => grepPool({ pattern: c.pattern, path: c.path, glob: c.glob })),
|
||||||
|
);
|
||||||
|
poolConcurrentTimes.push(performance.now() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subprocess rg sequential
|
||||||
|
const rgTimes: number[] = [];
|
||||||
|
let rgMatches = 0;
|
||||||
|
const rgDefaultArgs = ["--hidden", "--no-ignore", "--no-ignore-vcs"];
|
||||||
|
const globArg = c.glob ? ["-g", c.glob] : [];
|
||||||
|
|
||||||
|
const runRg = (): Promise<string> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const proc = spawn("rg", ["--json", ...rgDefaultArgs, ...globArg, c.pattern, c.path], {
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
});
|
||||||
|
let stdout = "";
|
||||||
|
proc.stdout.on("data", (data: Buffer) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
proc.on("close", () => resolve(stdout));
|
||||||
|
proc.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const countMatches = (result: string): number => {
|
||||||
|
const lines = result.split("\n").filter((l) => l.trim());
|
||||||
|
let matches = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
if (JSON.parse(line).type === "match") matches++;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = await runRg();
|
||||||
|
rgTimes.push(performance.now() - start);
|
||||||
|
rgMatches = countMatches(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subprocess rg concurrent (8x parallel)
|
||||||
|
const rgConcurrentTimes: number[] = [];
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
const start = performance.now();
|
||||||
|
await Promise.all(Array.from({ length: CONCURRENCY }, () => runRg()));
|
||||||
|
rgConcurrentTimes.push(performance.now() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avg = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||||
|
|
||||||
|
console.log(`${c.name}:`);
|
||||||
|
console.log(` Main thread: ${avg(mainTimes).toFixed(2)}ms (${mainMatches} matches)`);
|
||||||
|
console.log(` Main thread 8x: ${avg(mainConcurrentTimes).toFixed(2)}ms`);
|
||||||
|
console.log(` Worker pool: ${avg(poolTimes).toFixed(2)}ms (${poolMatches} matches)`);
|
||||||
|
console.log(` Worker pool 8x: ${avg(poolConcurrentTimes).toFixed(2)}ms`);
|
||||||
|
console.log(` Subprocess rg: ${avg(rgTimes).toFixed(2)}ms (${rgMatches} matches)`);
|
||||||
|
console.log(` Subprocess rg 8x: ${avg(rgConcurrentTimes).toFixed(2)}ms`);
|
||||||
|
|
||||||
|
const mainVsRg = avg(rgTimes) / avg(mainTimes);
|
||||||
|
const poolVsRgConcurrent = avg(rgConcurrentTimes) / avg(poolConcurrentTimes);
|
||||||
|
console.log(
|
||||||
|
` => Main thread is ${mainVsRg > 1 ? `${mainVsRg.toFixed(1)}x faster` : `${(1 / mainVsRg).toFixed(1)}x slower`} than rg (sequential)`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` => Worker pool is ${poolVsRgConcurrent > 1 ? `${poolVsRgConcurrent.toFixed(1)}x faster` : `${(1 / poolVsRgConcurrent).toFixed(1)}x slower`} than rg (8x concurrent)\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate();
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "@oh-my-pi/pi-natives",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Native Rust functionality compiled to WebAssembly via wasm-bindgen",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"wasm"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build:wasm": "bun scripts/build-wasm.ts",
|
||||||
|
"check": "biome check . && tsgo -p tsconfig.json",
|
||||||
|
"fix": "biome check --write --unsafe .",
|
||||||
|
"test": "bun test",
|
||||||
|
"bench": "bun bench/grep.ts"
|
||||||
|
},
|
||||||
|
"author": "Can Bölük",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
||||||
|
"directory": "packages/natives"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@oh-my-pi/pi-utils": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.3.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { $ } from "bun";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
|
||||||
|
const repoRoot = path.join(import.meta.dir, "../../..");
|
||||||
|
const rustDir = path.join(repoRoot, "crates/pi-natives");
|
||||||
|
const wasmDir = path.join(import.meta.dir, "../wasm");
|
||||||
|
|
||||||
|
await fs.mkdir(wasmDir, { recursive: true });
|
||||||
|
|
||||||
|
console.log("Building Rust WASM with wasm-bindgen...");
|
||||||
|
|
||||||
|
// Build with wasm-pack (wasm-opt disabled in Cargo.toml)
|
||||||
|
const buildResult = await $`wasm-pack build --target deno --release --out-dir pkg`
|
||||||
|
.cwd(rustDir)
|
||||||
|
.nothrow();
|
||||||
|
|
||||||
|
if (buildResult.exitCode !== 0) {
|
||||||
|
console.error("wasm-pack build failed");
|
||||||
|
console.error(buildResult.stderr.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkgDir = path.join(rustDir, "pkg");
|
||||||
|
|
||||||
|
// Crate name is pi-natives -> output files are pi_natives_*
|
||||||
|
const wasmSrc = path.join(pkgDir, "pi_natives_bg.wasm");
|
||||||
|
const wasmDst = path.join(wasmDir, "pi_natives_bg.wasm");
|
||||||
|
|
||||||
|
// Optimize with wasm-opt with bulk memory enabled
|
||||||
|
const optResult = await $`wasm-opt \
|
||||||
|
--enable-bulk-memory \
|
||||||
|
--enable-nontrapping-float-to-int \
|
||||||
|
-O3 \
|
||||||
|
--strip-debug \
|
||||||
|
${wasmSrc} \
|
||||||
|
-o ${wasmDst}`.nothrow();
|
||||||
|
|
||||||
|
if (optResult.exitCode !== 0) {
|
||||||
|
console.log("wasm-opt failed or not available, copying unoptimized build");
|
||||||
|
await fs.copyFile(wasmSrc, wasmDst);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy JS bindings
|
||||||
|
await fs.copyFile(path.join(pkgDir, "pi_natives.js"), path.join(wasmDir, "pi_natives.js"));
|
||||||
|
|
||||||
|
// Copy d.ts bindings
|
||||||
|
await fs.copyFile(path.join(pkgDir, "pi_natives.d.ts"), path.join(wasmDir, "pi_natives.d.ts"));
|
||||||
|
await fs.copyFile(path.join(pkgDir, "pi_natives_bg.wasm.d.ts"), path.join(wasmDir, "pi_natives_bg.wasm.d.ts"));
|
||||||
|
|
||||||
|
const stat = await fs.stat(wasmDst);
|
||||||
|
console.log(`Done: ${wasmDst} (${(stat.size / 1024).toFixed(0)}KB)`);
|
||||||
|
|
||||||
|
// Clean up pkg directory
|
||||||
|
await fs.rm(pkgDir, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
export interface TypeFilter {
|
||||||
|
extensions?: string[];
|
||||||
|
names?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_ALIASES: Record<string, TypeFilter> = {
|
||||||
|
js: { extensions: ["js", "jsx", "mjs", "cjs"] },
|
||||||
|
javascript: { extensions: ["js", "jsx", "mjs", "cjs"] },
|
||||||
|
ts: { extensions: ["ts", "tsx", "mts", "cts"] },
|
||||||
|
typescript: { extensions: ["ts", "tsx", "mts", "cts"] },
|
||||||
|
json: { extensions: ["json", "jsonc", "json5"] },
|
||||||
|
yaml: { extensions: ["yaml", "yml"] },
|
||||||
|
yml: { extensions: ["yaml", "yml"] },
|
||||||
|
toml: { extensions: ["toml"] },
|
||||||
|
md: { extensions: ["md", "markdown", "mdx"] },
|
||||||
|
markdown: { extensions: ["md", "markdown", "mdx"] },
|
||||||
|
py: { extensions: ["py", "pyi"] },
|
||||||
|
python: { extensions: ["py", "pyi"] },
|
||||||
|
rs: { extensions: ["rs"] },
|
||||||
|
rust: { extensions: ["rs"] },
|
||||||
|
go: { extensions: ["go"] },
|
||||||
|
java: { extensions: ["java"] },
|
||||||
|
kt: { extensions: ["kt", "kts"] },
|
||||||
|
kotlin: { extensions: ["kt", "kts"] },
|
||||||
|
c: { extensions: ["c", "h"] },
|
||||||
|
cpp: { extensions: ["cpp", "cc", "cxx", "hpp", "hxx", "hh"] },
|
||||||
|
cxx: { extensions: ["cpp", "cc", "cxx", "hpp", "hxx", "hh"] },
|
||||||
|
cs: { extensions: ["cs", "csx"] },
|
||||||
|
csharp: { extensions: ["cs", "csx"] },
|
||||||
|
php: { extensions: ["php", "phtml"] },
|
||||||
|
rb: { extensions: ["rb", "rake", "gemspec"] },
|
||||||
|
ruby: { extensions: ["rb", "rake", "gemspec"] },
|
||||||
|
sh: { extensions: ["sh", "bash", "zsh", "fish"] },
|
||||||
|
bash: { extensions: ["sh", "bash", "zsh"] },
|
||||||
|
zsh: { extensions: ["zsh"] },
|
||||||
|
fish: { extensions: ["fish"] },
|
||||||
|
html: { extensions: ["html", "htm"] },
|
||||||
|
css: { extensions: ["css"] },
|
||||||
|
scss: { extensions: ["scss"] },
|
||||||
|
sass: { extensions: ["sass"] },
|
||||||
|
less: { extensions: ["less"] },
|
||||||
|
xml: { extensions: ["xml"] },
|
||||||
|
docker: { names: ["dockerfile"] },
|
||||||
|
dockerfile: { names: ["dockerfile"] },
|
||||||
|
make: { names: ["makefile"] },
|
||||||
|
makefile: { names: ["makefile"] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildGlobPattern(glob?: string): string {
|
||||||
|
const trimmed = glob?.trim();
|
||||||
|
if (!trimmed) return "**/*";
|
||||||
|
const normalized = trimmed.replace(/\\/g, "/");
|
||||||
|
if (normalized.includes("/") || normalized.startsWith("**/")) return normalized;
|
||||||
|
return `**/${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTypeFilter(type?: string): TypeFilter | undefined {
|
||||||
|
if (!type) return undefined;
|
||||||
|
const trimmed = type.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const normalized = trimmed.toLowerCase();
|
||||||
|
const withoutDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
|
||||||
|
return TYPE_ALIASES[withoutDot] ?? { extensions: [withoutDot] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesTypeFilter(filePath: string, filter?: TypeFilter): boolean {
|
||||||
|
if (!filter) return true;
|
||||||
|
const baseName = path.basename(filePath).toLowerCase();
|
||||||
|
if (filter.names?.some(name => name.toLowerCase() === baseName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ext = path.extname(baseName).slice(1).toLowerCase();
|
||||||
|
if (!ext) return false;
|
||||||
|
return filter.extensions?.includes(ext) ?? false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* Native ripgrep wrapper using wasm-bindgen.
|
||||||
|
*
|
||||||
|
* JS handles filesystem operations (directory walking, file reading).
|
||||||
|
* WASM handles pure regex matching using ripgrep's engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||||
|
import {
|
||||||
|
CompiledPattern as WasmCompiledPattern,
|
||||||
|
has_match as wasmHasMatch,
|
||||||
|
search as wasmSearch,
|
||||||
|
} from "../../wasm/pi_natives";
|
||||||
|
import { WorkerPool } from "../pool";
|
||||||
|
import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
|
||||||
|
import type {
|
||||||
|
ContextLine,
|
||||||
|
GrepMatch,
|
||||||
|
GrepOptions,
|
||||||
|
GrepResult,
|
||||||
|
GrepSummary,
|
||||||
|
WasmSearchResult,
|
||||||
|
WorkerRequest,
|
||||||
|
WorkerResponse,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export type { ContextLine, GrepMatch, GrepOptions, GrepResult, GrepSummary };
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// File Walking
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function filterUndefined<T extends Record<string, unknown>>(obj: T): T {
|
||||||
|
const result = {} as T;
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
(result as Record<string, unknown>)[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Grep Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const GREP_WORKERS = (() => {
|
||||||
|
const val = process.env.OMP_GREP_WORKERS;
|
||||||
|
if (val === undefined) return true;
|
||||||
|
const n = Number.parseInt(val, 10);
|
||||||
|
return Number.isNaN(n) || n > 0;
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search files for a regex pattern (direct, single-threaded).
|
||||||
|
*/
|
||||||
|
async function grepDirect(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
|
||||||
|
const searchPath = path.resolve(options.path);
|
||||||
|
const outputMode = options.mode ?? "content";
|
||||||
|
const wasmMode = outputMode === "content" ? "content" : "count";
|
||||||
|
|
||||||
|
const stat = await fs.stat(searchPath);
|
||||||
|
const isFile = stat.isFile();
|
||||||
|
|
||||||
|
using compiledPattern = new WasmCompiledPattern(
|
||||||
|
filterUndefined({
|
||||||
|
pattern: options.pattern,
|
||||||
|
ignoreCase: options.ignoreCase,
|
||||||
|
multiline: options.multiline,
|
||||||
|
context: options.context,
|
||||||
|
maxColumns: options.maxColumns,
|
||||||
|
mode: wasmMode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const typeFilter = resolveTypeFilter(options.type);
|
||||||
|
const globPattern = buildGlobPattern(options.glob);
|
||||||
|
|
||||||
|
const matches: GrepMatch[] = [];
|
||||||
|
let totalMatches = 0;
|
||||||
|
let filesWithMatches = 0;
|
||||||
|
let filesSearched = 0;
|
||||||
|
let limitReached = false;
|
||||||
|
const maxCount = options.maxCount;
|
||||||
|
const globalOffset = options.offset ?? 0;
|
||||||
|
|
||||||
|
if (isFile) {
|
||||||
|
if (typeFilter && !matchesTypeFilter(searchPath, typeFilter)) {
|
||||||
|
return {
|
||||||
|
matches,
|
||||||
|
totalMatches,
|
||||||
|
filesWithMatches,
|
||||||
|
filesSearched,
|
||||||
|
limitReached: limitReached || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = Bun.mmap(searchPath);
|
||||||
|
filesSearched = 1;
|
||||||
|
|
||||||
|
const result = compiledPattern.search_bytes(
|
||||||
|
content,
|
||||||
|
maxCount,
|
||||||
|
globalOffset > 0 ? globalOffset : undefined,
|
||||||
|
) as WasmSearchResult;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.matchCount > 0) {
|
||||||
|
filesWithMatches = 1;
|
||||||
|
totalMatches = result.matchCount;
|
||||||
|
|
||||||
|
if (outputMode === "content") {
|
||||||
|
for (const m of result.matches) {
|
||||||
|
const match: GrepMatch = {
|
||||||
|
path: searchPath,
|
||||||
|
lineNumber: m.lineNumber,
|
||||||
|
line: m.line,
|
||||||
|
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||||
|
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||||
|
truncated: m.truncated || undefined,
|
||||||
|
};
|
||||||
|
matches.push(match);
|
||||||
|
onMatch?.(match);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const match: GrepMatch = {
|
||||||
|
path: searchPath,
|
||||||
|
lineNumber: 0,
|
||||||
|
line: "",
|
||||||
|
matchCount: result.matchCount,
|
||||||
|
};
|
||||||
|
matches.push(match);
|
||||||
|
onMatch?.(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
limitReached = result.limitReached || (maxCount !== undefined && totalMatches >= maxCount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const paths = await globPaths(globPattern, {
|
||||||
|
cwd: searchPath,
|
||||||
|
dot: options.hidden ?? true,
|
||||||
|
onlyFiles: true,
|
||||||
|
gitignore: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const relativePath of paths) {
|
||||||
|
if (limitReached) break;
|
||||||
|
if (typeFilter && !matchesTypeFilter(relativePath, typeFilter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = relativePath.replace(/\\/g, "/");
|
||||||
|
const fullPath = path.join(searchPath, normalizedPath);
|
||||||
|
|
||||||
|
let content: Uint8Array;
|
||||||
|
try {
|
||||||
|
content = Bun.mmap(fullPath);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
filesSearched++;
|
||||||
|
|
||||||
|
if (!compiledPattern.has_match_bytes(content)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileOffset = globalOffset > 0 ? Math.max(globalOffset - totalMatches, 0) : 0;
|
||||||
|
const remaining = maxCount !== undefined ? Math.max(maxCount - totalMatches, 0) : undefined;
|
||||||
|
if (remaining === 0) {
|
||||||
|
limitReached = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const result = compiledPattern.search_bytes(
|
||||||
|
content,
|
||||||
|
remaining,
|
||||||
|
fileOffset > 0 ? fileOffset : undefined,
|
||||||
|
) as WasmSearchResult;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.matchCount > 0) {
|
||||||
|
filesWithMatches++;
|
||||||
|
totalMatches += result.matchCount;
|
||||||
|
|
||||||
|
if (outputMode === "content") {
|
||||||
|
for (const m of result.matches) {
|
||||||
|
const match: GrepMatch = {
|
||||||
|
path: normalizedPath,
|
||||||
|
lineNumber: m.lineNumber,
|
||||||
|
line: m.line,
|
||||||
|
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||||
|
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||||
|
truncated: m.truncated || undefined,
|
||||||
|
};
|
||||||
|
matches.push(match);
|
||||||
|
onMatch?.(match);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const match: GrepMatch = {
|
||||||
|
path: normalizedPath,
|
||||||
|
lineNumber: 0,
|
||||||
|
line: "",
|
||||||
|
matchCount: result.matchCount,
|
||||||
|
};
|
||||||
|
matches.push(match);
|
||||||
|
onMatch?.(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.limitReached || (maxCount !== undefined && totalMatches >= maxCount)) {
|
||||||
|
limitReached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
matches,
|
||||||
|
totalMatches,
|
||||||
|
filesWithMatches,
|
||||||
|
filesSearched,
|
||||||
|
limitReached: limitReached || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Content Search (lower-level API)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a single file's content for a pattern.
|
||||||
|
* Lower-level API for when you already have file content.
|
||||||
|
*/
|
||||||
|
export function searchContent(
|
||||||
|
content: string,
|
||||||
|
options: {
|
||||||
|
pattern: string;
|
||||||
|
ignoreCase?: boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
maxCount?: number;
|
||||||
|
offset?: number;
|
||||||
|
context?: number;
|
||||||
|
maxColumns?: number;
|
||||||
|
mode?: "content" | "count";
|
||||||
|
},
|
||||||
|
): WasmSearchResult {
|
||||||
|
return wasmSearch(content, filterUndefined(options)) as WasmSearchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick check if content contains a pattern match.
|
||||||
|
*/
|
||||||
|
export function hasMatch(
|
||||||
|
content: string,
|
||||||
|
pattern: string,
|
||||||
|
options?: { ignoreCase?: boolean; multiline?: boolean },
|
||||||
|
): boolean {
|
||||||
|
return wasmHasMatch(content, pattern, options?.ignoreCase ?? false, options?.multiline ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Public API
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search files for a regex pattern.
|
||||||
|
*
|
||||||
|
* Uses worker pool by default. Set `OMP_GREP_WORKERS=0` to disable.
|
||||||
|
*/
|
||||||
|
export async function grep(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
|
||||||
|
if (GREP_WORKERS) {
|
||||||
|
return await grepPoolInternal(options);
|
||||||
|
}
|
||||||
|
return await grepDirect(options, onMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search files using worker pool (always, ignores OMP_GREP_WORKERS).
|
||||||
|
*/
|
||||||
|
export async function grepPool(options: GrepOptions): Promise<GrepResult> {
|
||||||
|
return await grepPoolInternal(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Worker Pool
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const pool = new WorkerPool<WorkerRequest, WorkerResponse>({
|
||||||
|
workerUrl: new URL("./worker.ts", import.meta.url).href,
|
||||||
|
maxWorkers: 4,
|
||||||
|
idleTimeoutMs: 30_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function grepPoolInternal(request: GrepOptions): Promise<GrepResult> {
|
||||||
|
const response = await pool.request<Extract<WorkerResponse, { type: "result" }>>({
|
||||||
|
type: "grep",
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminate all grep workers. */
|
||||||
|
export function terminate(): void {
|
||||||
|
pool.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { grepDirect };
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/** Options for searching files. */
|
||||||
|
export interface GrepOptions {
|
||||||
|
/** Regex pattern to search for */
|
||||||
|
pattern: string;
|
||||||
|
/** Directory or file to search */
|
||||||
|
path: string;
|
||||||
|
/** Glob filter for filenames (e.g., "*.ts") */
|
||||||
|
glob?: string;
|
||||||
|
/** Filter by file type (e.g., "js", "py", "rust") */
|
||||||
|
type?: string;
|
||||||
|
/** Case-insensitive search */
|
||||||
|
ignoreCase?: boolean;
|
||||||
|
/** Enable multiline matching */
|
||||||
|
multiline?: boolean;
|
||||||
|
/** Include hidden files (default: true) */
|
||||||
|
hidden?: boolean;
|
||||||
|
/** Maximum number of matches to return */
|
||||||
|
maxCount?: number;
|
||||||
|
/** Skip first N matches */
|
||||||
|
offset?: number;
|
||||||
|
/** Lines of context before/after matches */
|
||||||
|
context?: number;
|
||||||
|
/** Truncate lines longer than this (characters) */
|
||||||
|
maxColumns?: number;
|
||||||
|
/** Output mode */
|
||||||
|
mode?: "content" | "filesWithMatches" | "count";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextLine {
|
||||||
|
lineNumber: number;
|
||||||
|
line: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrepMatch {
|
||||||
|
path: string;
|
||||||
|
lineNumber: number;
|
||||||
|
line: string;
|
||||||
|
contextBefore?: ContextLine[];
|
||||||
|
contextAfter?: ContextLine[];
|
||||||
|
truncated?: boolean;
|
||||||
|
matchCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrepSummary {
|
||||||
|
totalMatches: number;
|
||||||
|
filesWithMatches: number;
|
||||||
|
filesSearched: number;
|
||||||
|
limitReached?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrepResult extends GrepSummary {
|
||||||
|
matches: GrepMatch[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WASM match result from the compiled pattern. */
|
||||||
|
export interface WasmMatch {
|
||||||
|
lineNumber: number;
|
||||||
|
line: string;
|
||||||
|
contextBefore: ContextLine[];
|
||||||
|
contextAfter: ContextLine[];
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WASM search result. */
|
||||||
|
export interface WasmSearchResult {
|
||||||
|
matches: WasmMatch[];
|
||||||
|
matchCount: number;
|
||||||
|
limitReached: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Message types from main thread to worker. */
|
||||||
|
export type WorkerRequest =
|
||||||
|
| { type: "init"; id: number }
|
||||||
|
| { type: "grep"; id: number; request: GrepOptions }
|
||||||
|
| { type: "destroy" };
|
||||||
|
|
||||||
|
/** Message types from worker to main thread. */
|
||||||
|
export type WorkerResponse =
|
||||||
|
| { type: "ready"; id: number }
|
||||||
|
| { type: "result"; id: number; result: GrepResult }
|
||||||
|
| { type: "error"; id: number; error: string };
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* Worker script for running wasm-bindgen grep.
|
||||||
|
* Each worker loads its own WASM instance and processes requests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||||
|
import { CompiledPattern } from "../../wasm/pi_natives";
|
||||||
|
import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
|
||||||
|
import type { GrepMatch, GrepOptions, GrepResult, WasmSearchResult, WorkerRequest, WorkerResponse } from "./types";
|
||||||
|
|
||||||
|
function filterUndefined<T extends Record<string, unknown>>(obj: T): T {
|
||||||
|
const result = {} as T;
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
(result as Record<string, unknown>)[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runGrep(request: GrepOptions): Promise<GrepResult> {
|
||||||
|
const searchPath = path.resolve(request.path);
|
||||||
|
const stat = await fs.stat(searchPath);
|
||||||
|
const isFile = stat.isFile();
|
||||||
|
|
||||||
|
using compiledPattern = new CompiledPattern(
|
||||||
|
filterUndefined({
|
||||||
|
pattern: request.pattern,
|
||||||
|
ignoreCase: request.ignoreCase,
|
||||||
|
multiline: request.multiline,
|
||||||
|
context: request.context,
|
||||||
|
maxColumns: request.maxColumns,
|
||||||
|
mode: request.mode === "content" || !request.mode ? "content" : "count",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const matches: GrepMatch[] = [];
|
||||||
|
let totalMatches = 0;
|
||||||
|
let filesWithMatches = 0;
|
||||||
|
let filesSearched = 0;
|
||||||
|
let limitReached = false;
|
||||||
|
const maxCount = request.maxCount;
|
||||||
|
const globalOffset = request.offset ?? 0;
|
||||||
|
const typeFilter = resolveTypeFilter(request.type);
|
||||||
|
const globPattern = buildGlobPattern(request.glob);
|
||||||
|
|
||||||
|
if (isFile) {
|
||||||
|
if (typeFilter && !matchesTypeFilter(searchPath, typeFilter)) {
|
||||||
|
return {
|
||||||
|
matches,
|
||||||
|
totalMatches,
|
||||||
|
filesWithMatches,
|
||||||
|
filesSearched,
|
||||||
|
limitReached: limitReached || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = Bun.mmap(searchPath);
|
||||||
|
filesSearched = 1;
|
||||||
|
|
||||||
|
const result = compiledPattern.search_bytes(
|
||||||
|
content,
|
||||||
|
maxCount,
|
||||||
|
globalOffset > 0 ? globalOffset : undefined,
|
||||||
|
) as WasmSearchResult;
|
||||||
|
|
||||||
|
if (!result.error && result.matchCount > 0) {
|
||||||
|
filesWithMatches = 1;
|
||||||
|
totalMatches = result.matchCount;
|
||||||
|
|
||||||
|
if (request.mode === "content" || !request.mode) {
|
||||||
|
for (const m of result.matches) {
|
||||||
|
matches.push({
|
||||||
|
path: searchPath,
|
||||||
|
lineNumber: m.lineNumber,
|
||||||
|
line: m.line,
|
||||||
|
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||||
|
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||||
|
truncated: m.truncated || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matches.push({
|
||||||
|
path: searchPath,
|
||||||
|
lineNumber: 0,
|
||||||
|
line: "",
|
||||||
|
matchCount: result.matchCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
limitReached = result.limitReached || (maxCount !== undefined && totalMatches >= maxCount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const paths = await globPaths(globPattern, {
|
||||||
|
cwd: searchPath,
|
||||||
|
dot: request.hidden ?? true,
|
||||||
|
onlyFiles: true,
|
||||||
|
gitignore: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const relativePath of paths) {
|
||||||
|
if (limitReached) break;
|
||||||
|
if (typeFilter && !matchesTypeFilter(relativePath, typeFilter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = relativePath.replace(/\\/g, "/");
|
||||||
|
const fullPath = path.join(searchPath, normalizedPath);
|
||||||
|
|
||||||
|
let content: Uint8Array;
|
||||||
|
try {
|
||||||
|
content = Bun.mmap(fullPath);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
filesSearched++;
|
||||||
|
|
||||||
|
if (!compiledPattern.has_match_bytes(content)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileOffset = globalOffset > 0 ? Math.max(globalOffset - totalMatches, 0) : 0;
|
||||||
|
const remaining = maxCount !== undefined ? Math.max(maxCount - totalMatches, 0) : undefined;
|
||||||
|
if (remaining === 0) {
|
||||||
|
limitReached = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const result = compiledPattern.search_bytes(
|
||||||
|
content,
|
||||||
|
remaining,
|
||||||
|
fileOffset > 0 ? fileOffset : undefined,
|
||||||
|
) as WasmSearchResult;
|
||||||
|
|
||||||
|
if (result.error) continue;
|
||||||
|
|
||||||
|
if (result.matchCount > 0) {
|
||||||
|
filesWithMatches++;
|
||||||
|
totalMatches += result.matchCount;
|
||||||
|
|
||||||
|
if (request.mode === "content" || !request.mode) {
|
||||||
|
for (const m of result.matches) {
|
||||||
|
matches.push({
|
||||||
|
path: normalizedPath,
|
||||||
|
lineNumber: m.lineNumber,
|
||||||
|
line: m.line,
|
||||||
|
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||||
|
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||||
|
truncated: m.truncated || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matches.push({
|
||||||
|
path: normalizedPath,
|
||||||
|
lineNumber: 0,
|
||||||
|
line: "",
|
||||||
|
matchCount: result.matchCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.limitReached || (maxCount !== undefined && totalMatches >= maxCount)) {
|
||||||
|
limitReached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
matches,
|
||||||
|
totalMatches,
|
||||||
|
filesWithMatches,
|
||||||
|
filesSearched,
|
||||||
|
limitReached: limitReached || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const self: Worker;
|
||||||
|
|
||||||
|
self.addEventListener("message", async (e: MessageEvent<WorkerRequest>) => {
|
||||||
|
const msg = e.data;
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "init":
|
||||||
|
self.postMessage({ type: "ready", id: msg.id } satisfies WorkerResponse);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "grep":
|
||||||
|
try {
|
||||||
|
const result = await runGrep(msg.request);
|
||||||
|
self.postMessage({ type: "result", id: msg.id, result } satisfies WorkerResponse);
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({
|
||||||
|
type: "error",
|
||||||
|
id: msg.id,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
} satisfies WorkerResponse);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "destroy":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Async image processing via worker.
|
||||||
|
*
|
||||||
|
* All heavy lifting happens in a worker thread to avoid blocking the main thread.
|
||||||
|
* Uses transferable ArrayBuffers to avoid copying image data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WorkerPool } from "../pool";
|
||||||
|
import type { ImageRequest, ImageResponse } from "./types";
|
||||||
|
|
||||||
|
// Re-export the enum for filter selection
|
||||||
|
export { SamplingFilter } from "../../wasm/pi_natives";
|
||||||
|
|
||||||
|
const pool = new WorkerPool<ImageRequest, ImageResponse>({
|
||||||
|
workerUrl: new URL("./worker.ts", import.meta.url).href,
|
||||||
|
maxWorkers: 1,
|
||||||
|
idleTimeoutMs: 0, // Keep alive - stateful (image handles)
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image handle for async operations.
|
||||||
|
* Must call free() when done to release WASM memory.
|
||||||
|
*/
|
||||||
|
export class PhotonImage {
|
||||||
|
#handle: number;
|
||||||
|
#width: number;
|
||||||
|
#height: number;
|
||||||
|
#freed = false;
|
||||||
|
|
||||||
|
private constructor(handle: number, width: number, height: number) {
|
||||||
|
this.#handle = handle;
|
||||||
|
this.#width = width;
|
||||||
|
this.#height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
static _create(handle: number, width: number, height: number): PhotonImage {
|
||||||
|
return new PhotonImage(handle, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_getHandle(): number {
|
||||||
|
if (this.#freed) throw new Error("Image already freed");
|
||||||
|
return this.#handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load an image from encoded bytes (PNG, JPEG, WebP, GIF).
|
||||||
|
* The bytes are transferred to the worker (zero-copy).
|
||||||
|
*/
|
||||||
|
static async new_from_byteslice(bytes: Uint8Array): Promise<PhotonImage> {
|
||||||
|
const response = await pool.request<Extract<ImageResponse, { type: "loaded" }>>({ type: "load", bytes }, [
|
||||||
|
bytes.buffer,
|
||||||
|
]);
|
||||||
|
return new PhotonImage(response.handle, response.width, response.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get image width in pixels. */
|
||||||
|
get_width(): number {
|
||||||
|
return this.#width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get image height in pixels. */
|
||||||
|
get_height(): number {
|
||||||
|
return this.#height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export as PNG bytes. */
|
||||||
|
async get_bytes(): Promise<Uint8Array> {
|
||||||
|
if (this.#freed) throw new Error("Image already freed");
|
||||||
|
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
||||||
|
type: "get_png",
|
||||||
|
handle: this.#handle,
|
||||||
|
});
|
||||||
|
return response.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export as JPEG bytes with specified quality (0-100). */
|
||||||
|
async get_bytes_jpeg(quality: number): Promise<Uint8Array> {
|
||||||
|
if (this.#freed) throw new Error("Image already freed");
|
||||||
|
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
||||||
|
type: "get_jpeg",
|
||||||
|
handle: this.#handle,
|
||||||
|
quality,
|
||||||
|
});
|
||||||
|
return response.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release WASM memory. Must be called when done with the image. */
|
||||||
|
free() {
|
||||||
|
if (this.#freed) return;
|
||||||
|
this.#freed = true;
|
||||||
|
pool.request({ type: "free", handle: this.#handle }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias for free() to support using-declarations. */
|
||||||
|
[Symbol.dispose](): void {
|
||||||
|
this.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image to the specified dimensions.
|
||||||
|
* Returns a new PhotonImage (original is not modified).
|
||||||
|
*/
|
||||||
|
export async function resize(image: PhotonImage, width: number, height: number, filter: number): Promise<PhotonImage> {
|
||||||
|
const handle = image._getHandle();
|
||||||
|
const response = await pool.request<Extract<ImageResponse, { type: "resized" }>>({
|
||||||
|
type: "resize",
|
||||||
|
handle,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
return PhotonImage._create(response.handle, response.width, response.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminate the image worker.
|
||||||
|
* Call this when shutting down to clean up resources.
|
||||||
|
*/
|
||||||
|
export function terminate(): void {
|
||||||
|
pool.terminate();
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Types for image worker communication.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ImageRequest =
|
||||||
|
| { type: "init"; id: number }
|
||||||
|
| { type: "destroy" }
|
||||||
|
| {
|
||||||
|
type: "load";
|
||||||
|
id: number;
|
||||||
|
/** Image bytes (transferred, not copied) */
|
||||||
|
bytes: Uint8Array;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "resize";
|
||||||
|
id: number;
|
||||||
|
/** Handle returned from load */
|
||||||
|
handle: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
filter: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "get_dimensions";
|
||||||
|
id: number;
|
||||||
|
handle: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "get_png";
|
||||||
|
id: number;
|
||||||
|
handle: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "get_jpeg";
|
||||||
|
id: number;
|
||||||
|
handle: number;
|
||||||
|
quality: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "free";
|
||||||
|
id: number;
|
||||||
|
handle: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageResponse =
|
||||||
|
| { type: "ready"; id: number }
|
||||||
|
| { type: "error"; id: number; error: string }
|
||||||
|
| { type: "loaded"; id: number; handle: number; width: number; height: number }
|
||||||
|
| { type: "resized"; id: number; handle: number; width: number; height: number }
|
||||||
|
| { type: "dimensions"; id: number; width: number; height: number }
|
||||||
|
| { type: "bytes"; id: number; bytes: Uint8Array }
|
||||||
|
| { type: "freed"; id: number };
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Worker for image processing operations.
|
||||||
|
* Uses WASM for actual processing, communicates via transferable buffers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PhotonImage, type SamplingFilter, resize as wasmResize } from "../../wasm/pi_natives";
|
||||||
|
import type { ImageRequest, ImageResponse } from "./types";
|
||||||
|
|
||||||
|
declare const self: Worker;
|
||||||
|
|
||||||
|
/** Map of handle -> PhotonImage */
|
||||||
|
const images = new Map<number, PhotonImage>();
|
||||||
|
let nextHandle = 1;
|
||||||
|
|
||||||
|
function respond(msg: ImageResponse, transfer?: ArrayBufferLike[]): void {
|
||||||
|
if (transfer) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
self.postMessage(msg, transfer as any);
|
||||||
|
} else {
|
||||||
|
self.postMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener("message", (e: MessageEvent<ImageRequest>) => {
|
||||||
|
const msg = e.data;
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "init":
|
||||||
|
respond({ type: "ready", id: msg.id });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "destroy":
|
||||||
|
for (const img of images.values()) {
|
||||||
|
img.free();
|
||||||
|
}
|
||||||
|
images.clear();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "load": {
|
||||||
|
try {
|
||||||
|
const img = PhotonImage.new_from_byteslice(msg.bytes);
|
||||||
|
const handle = nextHandle++;
|
||||||
|
images.set(handle, img);
|
||||||
|
respond({
|
||||||
|
type: "loaded",
|
||||||
|
id: msg.id,
|
||||||
|
handle,
|
||||||
|
width: img.get_width(),
|
||||||
|
height: img.get_height(),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
respond({
|
||||||
|
type: "error",
|
||||||
|
id: msg.id,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "resize": {
|
||||||
|
const img = images.get(msg.handle);
|
||||||
|
if (!img) {
|
||||||
|
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const filter = msg.filter as SamplingFilter;
|
||||||
|
const resized = wasmResize(img, msg.width, msg.height, filter);
|
||||||
|
const handle = nextHandle++;
|
||||||
|
images.set(handle, resized);
|
||||||
|
respond({
|
||||||
|
type: "resized",
|
||||||
|
id: msg.id,
|
||||||
|
handle,
|
||||||
|
width: resized.get_width(),
|
||||||
|
height: resized.get_height(),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
respond({
|
||||||
|
type: "error",
|
||||||
|
id: msg.id,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get_dimensions": {
|
||||||
|
const img = images.get(msg.handle);
|
||||||
|
if (!img) {
|
||||||
|
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
respond({
|
||||||
|
type: "dimensions",
|
||||||
|
id: msg.id,
|
||||||
|
width: img.get_width(),
|
||||||
|
height: img.get_height(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get_png": {
|
||||||
|
const img = images.get(msg.handle);
|
||||||
|
if (!img) {
|
||||||
|
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const bytes = img.get_bytes();
|
||||||
|
respond({ type: "bytes", id: msg.id, bytes }, [bytes.buffer]);
|
||||||
|
} catch (err) {
|
||||||
|
respond({
|
||||||
|
type: "error",
|
||||||
|
id: msg.id,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get_jpeg": {
|
||||||
|
const img = images.get(msg.handle);
|
||||||
|
if (!img) {
|
||||||
|
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const bytes = img.get_bytes_jpeg(msg.quality);
|
||||||
|
respond({ type: "bytes", id: msg.id, bytes }, [bytes.buffer]);
|
||||||
|
} catch (err) {
|
||||||
|
respond({
|
||||||
|
type: "error",
|
||||||
|
id: msg.id,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "free": {
|
||||||
|
const img = images.get(msg.handle);
|
||||||
|
if (img) {
|
||||||
|
img.free();
|
||||||
|
images.delete(msg.handle);
|
||||||
|
}
|
||||||
|
respond({ type: "freed", id: msg.id });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Native utilities powered by WASM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Grep (ripgrep-based regex search)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ContextLine,
|
||||||
|
type GrepMatch,
|
||||||
|
type GrepOptions,
|
||||||
|
type GrepResult,
|
||||||
|
type GrepSummary,
|
||||||
|
grep,
|
||||||
|
grepDirect,
|
||||||
|
grepPool,
|
||||||
|
hasMatch,
|
||||||
|
searchContent,
|
||||||
|
terminate,
|
||||||
|
} from "./grep/index";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// WASI implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export { WASI1, WASIError, WASIExitError, type WASIOptions } from "./wasix";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Find (file discovery)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface FindOptions {
|
||||||
|
/** Glob pattern to match (e.g., `*.ts`) */
|
||||||
|
pattern: string;
|
||||||
|
/** Directory to search */
|
||||||
|
path: string;
|
||||||
|
/** Filter by file type: "file", "dir", or "symlink" */
|
||||||
|
fileType?: "file" | "dir" | "symlink";
|
||||||
|
/** Include hidden files (default: false) */
|
||||||
|
hidden?: boolean;
|
||||||
|
/** Maximum number of results */
|
||||||
|
maxResults?: number;
|
||||||
|
/** Respect .gitignore files (default: true) */
|
||||||
|
gitignore?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindMatch {
|
||||||
|
path: string;
|
||||||
|
fileType: "file" | "dir" | "symlink";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindResult {
|
||||||
|
matches: FindMatch[];
|
||||||
|
totalMatches: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find files matching a glob pattern.
|
||||||
|
* Respects .gitignore by default.
|
||||||
|
*/
|
||||||
|
export async function find(options: FindOptions, onMatch?: (match: FindMatch) => void): Promise<FindResult> {
|
||||||
|
const searchPath = path.resolve(options.path);
|
||||||
|
const pattern = options.pattern || "*";
|
||||||
|
|
||||||
|
// Convert simple patterns to recursive globs if needed
|
||||||
|
const globPattern = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
|
||||||
|
|
||||||
|
const paths = await globPaths(globPattern, {
|
||||||
|
cwd: searchPath,
|
||||||
|
dot: options.hidden ?? false,
|
||||||
|
onlyFiles: options.fileType === "file",
|
||||||
|
gitignore: options.gitignore ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const matches: FindMatch[] = [];
|
||||||
|
const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
for (const p of paths) {
|
||||||
|
if (matches.length >= maxResults) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = p.replace(/\\/g, "/");
|
||||||
|
if (!normalizedPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats: Awaited<ReturnType<typeof fs.lstat>>;
|
||||||
|
try {
|
||||||
|
stats = await fs.lstat(path.join(searchPath, normalizedPath));
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileType: "file" | "dir" | "symlink" = stats.isSymbolicLink()
|
||||||
|
? "symlink"
|
||||||
|
: stats.isDirectory()
|
||||||
|
? "dir"
|
||||||
|
: "file";
|
||||||
|
|
||||||
|
if (options.fileType && options.fileType !== fileType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match: FindMatch = {
|
||||||
|
path: normalizedPath,
|
||||||
|
fileType,
|
||||||
|
};
|
||||||
|
|
||||||
|
matches.push(match);
|
||||||
|
onMatch?.(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
matches,
|
||||||
|
totalMatches: matches.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Image processing (photon-compatible API)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export {
|
||||||
|
PhotonImage,
|
||||||
|
resize,
|
||||||
|
SamplingFilter,
|
||||||
|
terminate as terminateImageWorker,
|
||||||
|
} from "./image/index";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Worker Pool (shared infrastructure)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export { type BaseRequest, type BaseResponse, WorkerPool, type WorkerPoolOptions } from "./pool";
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
/**
|
||||||
|
* Generic worker pool for WASM-based operations.
|
||||||
|
*
|
||||||
|
* Supports both single-worker (maxWorkers: 1) and multi-worker scenarios.
|
||||||
|
* Workers are lazily created and auto-terminated after idle timeout.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Base request type - workers must accept messages with this shape. */
|
||||||
|
export interface BaseRequest {
|
||||||
|
type: string;
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Base response type - workers must respond with this shape. */
|
||||||
|
export interface BaseResponse {
|
||||||
|
type: string;
|
||||||
|
id: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkerPoolOptions {
|
||||||
|
/** URL to the worker script. */
|
||||||
|
workerUrl: string | URL;
|
||||||
|
/** Maximum number of workers (default: 4). */
|
||||||
|
maxWorkers?: number;
|
||||||
|
/** Idle timeout in ms before terminating unused workers (0 = never, default: 30000). */
|
||||||
|
idleTimeoutMs?: number;
|
||||||
|
/** Timeout for worker initialization in ms (default: 10000). */
|
||||||
|
initTimeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PooledWorker {
|
||||||
|
worker: Worker;
|
||||||
|
busy: boolean;
|
||||||
|
lastUsed: number;
|
||||||
|
currentRequestId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingRequest<T> {
|
||||||
|
resolve: (result: T) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
timeout?: ReturnType<typeof setTimeout>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pool of workers that process requests in parallel.
|
||||||
|
*
|
||||||
|
* @typeParam TReq - Request message type (must extend BaseRequest)
|
||||||
|
* @typeParam TRes - Response message type (must extend BaseResponse)
|
||||||
|
*/
|
||||||
|
export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
|
||||||
|
readonly #options: Required<WorkerPoolOptions>;
|
||||||
|
readonly #pool: PooledWorker[] = [];
|
||||||
|
readonly #waiters: Array<(worker: PooledWorker) => void> = [];
|
||||||
|
readonly #pending = new Map<number, PendingRequest<TRes>>();
|
||||||
|
#nextRequestId = 1;
|
||||||
|
#idleCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
constructor(options: WorkerPoolOptions) {
|
||||||
|
this.#options = {
|
||||||
|
workerUrl: options.workerUrl,
|
||||||
|
maxWorkers: options.maxWorkers ?? 4,
|
||||||
|
idleTimeoutMs: options.idleTimeoutMs ?? 30_000,
|
||||||
|
initTimeoutMs: options.initTimeoutMs ?? 10_000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to a worker and wait for the response.
|
||||||
|
* Workers are acquired from the pool (or created if under limit).
|
||||||
|
*/
|
||||||
|
async request<T extends TRes = TRes>(
|
||||||
|
msg: TReq | (Omit<TReq, "id"> & { id?: number }),
|
||||||
|
transfer?: ArrayBufferLike[],
|
||||||
|
): Promise<T> {
|
||||||
|
const worker = await this.#acquireWorker();
|
||||||
|
const id = msg.id ?? this.#nextRequestId++;
|
||||||
|
const fullMsg = { ...msg, id } as TReq;
|
||||||
|
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
||||||
|
this.#pending.set(id, {
|
||||||
|
resolve: resolve as (result: TRes) => void,
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.currentRequestId = id;
|
||||||
|
if (transfer) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
worker.worker.postMessage(fullMsg, transfer as any);
|
||||||
|
} else {
|
||||||
|
worker.worker.postMessage(fullMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminate all workers and clear pending requests. */
|
||||||
|
terminate(): void {
|
||||||
|
if (this.#idleCheckInterval) {
|
||||||
|
clearInterval(this.#idleCheckInterval);
|
||||||
|
this.#idleCheckInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const w of [...this.#pool]) {
|
||||||
|
this.#removeWorker(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#waiters.length = 0;
|
||||||
|
|
||||||
|
for (const pending of this.#pending.values()) {
|
||||||
|
pending.reject(new Error("Worker pool terminated"));
|
||||||
|
if (pending.timeout) clearTimeout(pending.timeout);
|
||||||
|
}
|
||||||
|
this.#pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#createWorker(): PooledWorker {
|
||||||
|
const worker = new Worker(this.#options.workerUrl);
|
||||||
|
|
||||||
|
const pooledWorker: PooledWorker = {
|
||||||
|
worker,
|
||||||
|
busy: false,
|
||||||
|
lastUsed: Date.now(),
|
||||||
|
currentRequestId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<TRes>) => {
|
||||||
|
this.#handleMessage(pooledWorker, e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (e: ErrorEvent) => {
|
||||||
|
const requestId = pooledWorker.currentRequestId;
|
||||||
|
if (requestId !== null) {
|
||||||
|
this.#rejectRequest(requestId, new Error(`Worker error: ${e.message}`));
|
||||||
|
}
|
||||||
|
this.#removeWorker(pooledWorker);
|
||||||
|
};
|
||||||
|
|
||||||
|
return pooledWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleMessage(pooledWorker: PooledWorker, msg: TRes): void {
|
||||||
|
const pending = this.#pending.get(msg.id);
|
||||||
|
if (!pending) return;
|
||||||
|
|
||||||
|
this.#pending.delete(msg.id);
|
||||||
|
if (pending.timeout) clearTimeout(pending.timeout);
|
||||||
|
|
||||||
|
if (msg.type === "error" && "error" in msg) {
|
||||||
|
pending.reject(new Error(msg.error ?? "Unknown error"));
|
||||||
|
} else {
|
||||||
|
pending.resolve(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release worker back to pool (unless it was the init request)
|
||||||
|
if (msg.type !== "ready") {
|
||||||
|
pooledWorker.currentRequestId = null;
|
||||||
|
this.#releaseWorker(pooledWorker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#rejectRequest(id: number, error: Error): void {
|
||||||
|
const pending = this.#pending.get(id);
|
||||||
|
if (pending) {
|
||||||
|
this.#pending.delete(id);
|
||||||
|
if (pending.timeout) clearTimeout(pending.timeout);
|
||||||
|
pending.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#removeWorker(pooledWorker: PooledWorker): void {
|
||||||
|
const idx = this.#pool.indexOf(pooledWorker);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.#pool.splice(idx, 1);
|
||||||
|
}
|
||||||
|
pooledWorker.worker.postMessage({ type: "destroy" } satisfies BaseRequest);
|
||||||
|
pooledWorker.worker.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#releaseWorker(pooledWorker: PooledWorker): void {
|
||||||
|
pooledWorker.busy = false;
|
||||||
|
pooledWorker.lastUsed = Date.now();
|
||||||
|
|
||||||
|
if (this.#waiters.length) {
|
||||||
|
const waiter = this.#waiters.shift()!;
|
||||||
|
pooledWorker.busy = true;
|
||||||
|
waiter(pooledWorker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkIdleWorkers(): void {
|
||||||
|
if (this.#options.idleTimeoutMs === 0) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const toRemove: PooledWorker[] = [];
|
||||||
|
|
||||||
|
for (const w of this.#pool) {
|
||||||
|
if (!w.busy && now - w.lastUsed > this.#options.idleTimeoutMs && !this.#waiters.length) {
|
||||||
|
toRemove.push(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const w of toRemove) {
|
||||||
|
this.#removeWorker(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#pool.length === 0 && this.#idleCheckInterval) {
|
||||||
|
clearInterval(this.#idleCheckInterval);
|
||||||
|
this.#idleCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ensureIdleCheck(): void {
|
||||||
|
if (this.#options.idleTimeoutMs > 0 && !this.#idleCheckInterval) {
|
||||||
|
this.#idleCheckInterval = setInterval(() => this.#checkIdleWorkers(), 10_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #initializeWorker(pooledWorker: PooledWorker): Promise<void> {
|
||||||
|
const id = this.#nextRequestId++;
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.#rejectRequest(id, new Error("Worker initialization timeout"));
|
||||||
|
}, this.#options.initTimeoutMs);
|
||||||
|
|
||||||
|
this.#pending.set(id, {
|
||||||
|
resolve: () => resolve(),
|
||||||
|
reject,
|
||||||
|
timeout,
|
||||||
|
} as PendingRequest<TRes>);
|
||||||
|
|
||||||
|
pooledWorker.worker.postMessage({ type: "init", id } satisfies BaseRequest);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #acquireWorker(): Promise<PooledWorker> {
|
||||||
|
// Try to find an idle worker
|
||||||
|
for (const w of this.#pool) {
|
||||||
|
if (!w.busy) {
|
||||||
|
w.busy = true;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new worker if under limit
|
||||||
|
if (this.#pool.length < this.#options.maxWorkers) {
|
||||||
|
const worker = this.#createWorker();
|
||||||
|
worker.busy = true;
|
||||||
|
this.#pool.push(worker);
|
||||||
|
this.#ensureIdleCheck();
|
||||||
|
await this.#initializeWorker(worker);
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a worker to become available
|
||||||
|
const { promise, resolve } = Promise.withResolvers<PooledWorker>();
|
||||||
|
this.#waiters.push(w => {
|
||||||
|
w.busy = true;
|
||||||
|
resolve(w);
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { type FindMatch, find, grep } from "../src/index";
|
||||||
|
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
async function setupFixtures() {
|
||||||
|
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "natives-test-"));
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(testDir, "file1.ts"),
|
||||||
|
`export function hello() {
|
||||||
|
// TODO: implement
|
||||||
|
return "hello";
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(testDir, "file2.ts"),
|
||||||
|
`export function world() {
|
||||||
|
// FIXME: fix this
|
||||||
|
return "world";
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(testDir, "readme.md"),
|
||||||
|
`# Test README
|
||||||
|
|
||||||
|
This is a test file.
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupFixtures() {
|
||||||
|
await fs.rm(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("pi-natives", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupFixtures();
|
||||||
|
return async () => {
|
||||||
|
await cleanupFixtures();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("grep", () => {
|
||||||
|
it("should find patterns in files", async () => {
|
||||||
|
const result = await grep({
|
||||||
|
pattern: "TODO",
|
||||||
|
path: testDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.totalMatches).toBe(1);
|
||||||
|
expect(result.matches.length).toBe(1);
|
||||||
|
expect(result.matches[0].line).toContain("TODO");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect glob patterns", async () => {
|
||||||
|
const result = await grep({
|
||||||
|
pattern: "test",
|
||||||
|
path: testDir,
|
||||||
|
glob: "*.md",
|
||||||
|
ignoreCase: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.totalMatches).toBe(2); // "Test" in title + "test" in body
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return filesWithMatches mode", async () => {
|
||||||
|
const result = await grep({
|
||||||
|
pattern: "return",
|
||||||
|
path: testDir,
|
||||||
|
mode: "filesWithMatches",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.filesWithMatches).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("find", () => {
|
||||||
|
it("should find files matching pattern", async () => {
|
||||||
|
const result = await find({
|
||||||
|
pattern: "*.ts",
|
||||||
|
path: testDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.totalMatches).toBe(2);
|
||||||
|
expect(result.matches.every((m: FindMatch) => m.path.endsWith(".ts"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by file type", async () => {
|
||||||
|
const result = await find({
|
||||||
|
pattern: "*",
|
||||||
|
path: testDir,
|
||||||
|
fileType: "file",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.totalMatches).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "bench/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json"
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compiled regex matcher that can be reused across multiple searches.
|
||||||
|
*/
|
||||||
|
export class CompiledPattern {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* Check if content has any matches (faster than full search).
|
||||||
|
*/
|
||||||
|
has_match(content: string): boolean;
|
||||||
|
/**
|
||||||
|
* Check if bytes have any matches (faster than full search).
|
||||||
|
*/
|
||||||
|
has_match_bytes(content: Uint8Array): boolean;
|
||||||
|
/**
|
||||||
|
* Compile a regex pattern for reuse.
|
||||||
|
*/
|
||||||
|
constructor(options: any);
|
||||||
|
/**
|
||||||
|
* Search content using this compiled pattern.
|
||||||
|
* Returns matches as a JS object.
|
||||||
|
*/
|
||||||
|
search(content: string, max_count?: number | null, offset?: number | null): any;
|
||||||
|
/**
|
||||||
|
* Search bytes directly (avoids UTF-16 to UTF-8 conversion).
|
||||||
|
* Use with `Bun.mmap()` for best performance.
|
||||||
|
*/
|
||||||
|
search_bytes(content: Uint8Array, max_count?: number | null, offset?: number | null): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image container for WASM interop.
|
||||||
|
*/
|
||||||
|
export class PhotonImage {
|
||||||
|
private constructor();
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* Export image as PNG bytes.
|
||||||
|
*/
|
||||||
|
get_bytes(): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Export image as JPEG bytes with specified quality (0-100).
|
||||||
|
*/
|
||||||
|
get_bytes_jpeg(quality: number): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Get the height of the image.
|
||||||
|
*/
|
||||||
|
get_height(): number;
|
||||||
|
/**
|
||||||
|
* Get the width of the image.
|
||||||
|
*/
|
||||||
|
get_width(): number;
|
||||||
|
/**
|
||||||
|
* Create a new PhotonImage from encoded image bytes (PNG, JPEG, WebP, GIF).
|
||||||
|
*/
|
||||||
|
static new_from_byteslice(bytes: Uint8Array): PhotonImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sampling filter for resize operations.
|
||||||
|
*/
|
||||||
|
export enum SamplingFilter {
|
||||||
|
Nearest = 1,
|
||||||
|
Triangle = 2,
|
||||||
|
CatmullRom = 3,
|
||||||
|
Gaussian = 4,
|
||||||
|
Lanczos3 = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick check if content matches a pattern.
|
||||||
|
*/
|
||||||
|
export function has_match(content: string, pattern: string, ignore_case: boolean, multiline: boolean): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image to the specified dimensions.
|
||||||
|
*/
|
||||||
|
export function resize(image: PhotonImage, width: number, height: number, filter: SamplingFilter): PhotonImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search content for a pattern (one-shot, compiles pattern each time).
|
||||||
|
* For repeated searches with the same pattern, use [`CompiledPattern`].
|
||||||
|
*/
|
||||||
|
export function search(content: string, options: any): any;
|
||||||
@@ -0,0 +1,656 @@
|
|||||||
|
/* @ts-self-types="./pi_natives.d.ts" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compiled regex matcher that can be reused across multiple searches.
|
||||||
|
*/
|
||||||
|
export class CompiledPattern {
|
||||||
|
__destroy_into_raw() {
|
||||||
|
const ptr = this.__wbg_ptr;
|
||||||
|
this.__wbg_ptr = 0;
|
||||||
|
CompiledPatternFinalization.unregister(this);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_compiledpattern_free(ptr, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if content has any matches (faster than full search).
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
has_match(content) {
|
||||||
|
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.compiledpattern_has_match(this.__wbg_ptr, ptr0, len0);
|
||||||
|
return ret !== 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if bytes have any matches (faster than full search).
|
||||||
|
* @param {Uint8Array} content
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
has_match_bytes(content) {
|
||||||
|
const ptr0 = passArray8ToWasm0(content, wasm.__wbindgen_export);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.compiledpattern_has_match(this.__wbg_ptr, ptr0, len0);
|
||||||
|
return ret !== 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Compile a regex pattern for reuse.
|
||||||
|
* @param {any} options
|
||||||
|
*/
|
||||||
|
constructor(options) {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
wasm.compiledpattern_new(retptr, addHeapObject(options));
|
||||||
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||||
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||||
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||||
|
if (r2) {
|
||||||
|
throw takeObject(r1);
|
||||||
|
}
|
||||||
|
this.__wbg_ptr = r0 >>> 0;
|
||||||
|
CompiledPatternFinalization.register(this, this.__wbg_ptr, this);
|
||||||
|
return this;
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Search content using this compiled pattern.
|
||||||
|
* Returns matches as a JS object.
|
||||||
|
* @param {string} content
|
||||||
|
* @param {number | null} [max_count]
|
||||||
|
* @param {number | null} [offset]
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
search(content, max_count, offset) {
|
||||||
|
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.compiledpattern_search(this.__wbg_ptr, ptr0, len0, isLikeNone(max_count) ? 0x100000001 : (max_count) >>> 0, isLikeNone(offset) ? 0x100000001 : (offset) >>> 0);
|
||||||
|
return takeObject(ret);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Search bytes directly (avoids UTF-16 to UTF-8 conversion).
|
||||||
|
* Use with `Bun.mmap()` for best performance.
|
||||||
|
* @param {Uint8Array} content
|
||||||
|
* @param {number | null} [max_count]
|
||||||
|
* @param {number | null} [offset]
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
search_bytes(content, max_count, offset) {
|
||||||
|
const ptr0 = passArray8ToWasm0(content, wasm.__wbindgen_export);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.compiledpattern_search(this.__wbg_ptr, ptr0, len0, isLikeNone(max_count) ? 0x100000001 : (max_count) >>> 0, isLikeNone(offset) ? 0x100000001 : (offset) >>> 0);
|
||||||
|
return takeObject(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Symbol.dispose) CompiledPattern.prototype[Symbol.dispose] = CompiledPattern.prototype.free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image container for WASM interop.
|
||||||
|
*/
|
||||||
|
export class PhotonImage {
|
||||||
|
static __wrap(ptr) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
const obj = Object.create(PhotonImage.prototype);
|
||||||
|
obj.__wbg_ptr = ptr;
|
||||||
|
PhotonImageFinalization.register(obj, obj.__wbg_ptr, obj);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
__destroy_into_raw() {
|
||||||
|
const ptr = this.__wbg_ptr;
|
||||||
|
this.__wbg_ptr = 0;
|
||||||
|
PhotonImageFinalization.unregister(this);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_photonimage_free(ptr, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Export image as PNG bytes.
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
get_bytes() {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
wasm.photonimage_get_bytes(retptr, this.__wbg_ptr);
|
||||||
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||||
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||||
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||||
|
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
||||||
|
if (r3) {
|
||||||
|
throw takeObject(r2);
|
||||||
|
}
|
||||||
|
var v1 = getArrayU8FromWasm0(r0, r1).slice();
|
||||||
|
wasm.__wbindgen_export3(r0, r1 * 1, 1);
|
||||||
|
return v1;
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Export image as JPEG bytes with specified quality (0-100).
|
||||||
|
* @param {number} quality
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
get_bytes_jpeg(quality) {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
wasm.photonimage_get_bytes_jpeg(retptr, this.__wbg_ptr, quality);
|
||||||
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||||
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||||
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||||
|
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
||||||
|
if (r3) {
|
||||||
|
throw takeObject(r2);
|
||||||
|
}
|
||||||
|
var v1 = getArrayU8FromWasm0(r0, r1).slice();
|
||||||
|
wasm.__wbindgen_export3(r0, r1 * 1, 1);
|
||||||
|
return v1;
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the height of the image.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get_height() {
|
||||||
|
const ret = wasm.photonimage_get_height(this.__wbg_ptr);
|
||||||
|
return ret >>> 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the width of the image.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get_width() {
|
||||||
|
const ret = wasm.photonimage_get_width(this.__wbg_ptr);
|
||||||
|
return ret >>> 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create a new PhotonImage from encoded image bytes (PNG, JPEG, WebP, GIF).
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {PhotonImage}
|
||||||
|
*/
|
||||||
|
static new_from_byteslice(bytes) {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_export);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
wasm.photonimage_new_from_byteslice(retptr, ptr0, len0);
|
||||||
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||||
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||||
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||||
|
if (r2) {
|
||||||
|
throw takeObject(r1);
|
||||||
|
}
|
||||||
|
return PhotonImage.__wrap(r0);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Symbol.dispose) PhotonImage.prototype[Symbol.dispose] = PhotonImage.prototype.free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sampling filter for resize operations.
|
||||||
|
* @enum {1 | 2 | 3 | 4 | 5}
|
||||||
|
*/
|
||||||
|
export const SamplingFilter = Object.freeze({
|
||||||
|
Nearest: 1, "1": "Nearest",
|
||||||
|
Triangle: 2, "2": "Triangle",
|
||||||
|
CatmullRom: 3, "3": "CatmullRom",
|
||||||
|
Gaussian: 4, "4": "Gaussian",
|
||||||
|
Lanczos3: 5, "5": "Lanczos3",
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick check if content matches a pattern.
|
||||||
|
* @param {string} content
|
||||||
|
* @param {string} pattern
|
||||||
|
* @param {boolean} ignore_case
|
||||||
|
* @param {boolean} multiline
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function has_match(content, pattern, ignore_case, multiline) {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(pattern, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
wasm.has_match(retptr, ptr0, len0, ptr1, len1, ignore_case, multiline);
|
||||||
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||||
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||||
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||||
|
if (r2) {
|
||||||
|
throw takeObject(r1);
|
||||||
|
}
|
||||||
|
return r0 !== 0;
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image to the specified dimensions.
|
||||||
|
* @param {PhotonImage} image
|
||||||
|
* @param {number} width
|
||||||
|
* @param {number} height
|
||||||
|
* @param {SamplingFilter} filter
|
||||||
|
* @returns {PhotonImage}
|
||||||
|
*/
|
||||||
|
export function resize(image, width, height, filter) {
|
||||||
|
_assertClass(image, PhotonImage);
|
||||||
|
const ret = wasm.resize(image.__wbg_ptr, width, height, filter);
|
||||||
|
return PhotonImage.__wrap(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search content for a pattern (one-shot, compiles pattern each time).
|
||||||
|
* For repeated searches with the same pattern, use [`CompiledPattern`].
|
||||||
|
* @param {string} content
|
||||||
|
* @param {any} options
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export function search(content, options) {
|
||||||
|
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.search(ptr0, len0, addHeapObject(options));
|
||||||
|
return takeObject(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_get_imports() {
|
||||||
|
const import0 = {
|
||||||
|
__proto__: null,
|
||||||
|
__wbg_Error_8c4e43fe74559d73: function(arg0, arg1) {
|
||||||
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_Number_04624de7d0e8332d: function(arg0) {
|
||||||
|
const ret = Number(getObject(arg0));
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_String_8f0eb39a4a4c2f66: function(arg0, arg1) {
|
||||||
|
const ret = String(getObject(arg1));
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_bigint_get_as_i64_8fcf4ce7f1ca72a2: function(arg0, arg1) {
|
||||||
|
const v = getObject(arg1);
|
||||||
|
const ret = typeof(v) === 'bigint' ? v : undefined;
|
||||||
|
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_boolean_get_bbbb1c18aa2f5e25: function(arg0) {
|
||||||
|
const v = getObject(arg0);
|
||||||
|
const ret = typeof(v) === 'boolean' ? v : undefined;
|
||||||
|
return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_debug_string_0bc8482c6e3508ae: function(arg0, arg1) {
|
||||||
|
const ret = debugString(getObject(arg1));
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_in_47fa6863be6f2f25: function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg0) in getObject(arg1);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_bigint_31b12575b56f32fc: function(arg0) {
|
||||||
|
const ret = typeof(getObject(arg0)) === 'bigint';
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_object_5ae8e5880f2c1fbd: function(arg0) {
|
||||||
|
const val = getObject(arg0);
|
||||||
|
const ret = typeof(val) === 'object' && val !== null;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_string_cd444516edc5b180: function(arg0) {
|
||||||
|
const ret = typeof(getObject(arg0)) === 'string';
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_undefined_9e4d92534c42d778: function(arg0) {
|
||||||
|
const ret = getObject(arg0) === undefined;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_jsval_eq_11888390b0186270: function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg0) === getObject(arg1);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_jsval_loose_eq_9dd77d8cd6671811: function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg0) == getObject(arg1);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_number_get_8ff4255516ccad3e: function(arg0, arg1) {
|
||||||
|
const obj = getObject(arg1);
|
||||||
|
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||||
|
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_string_get_72fb696202c56729: function(arg0, arg1) {
|
||||||
|
const obj = getObject(arg1);
|
||||||
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||||
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_throw_be289d5034ed271b: function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
},
|
||||||
|
__wbg_entries_58c7934c745daac7: function(arg0) {
|
||||||
|
const ret = Object.entries(getObject(arg0));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_error_7534b8e9a36f1ab4: function(arg0, arg1) {
|
||||||
|
let deferred0_0;
|
||||||
|
let deferred0_1;
|
||||||
|
try {
|
||||||
|
deferred0_0 = arg0;
|
||||||
|
deferred0_1 = arg1;
|
||||||
|
console.error(getStringFromWasm0(arg0, arg1));
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_export3(deferred0_0, deferred0_1, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
__wbg_get_9b94d73e6221f75c: function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg0)[arg1 >>> 0];
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_get_with_ref_key_1dc361bd10053bfe: function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg0)[getObject(arg1)];
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_instanceof_ArrayBuffer_c367199e2fa2aa04: function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = getObject(arg0) instanceof ArrayBuffer;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_instanceof_Uint8Array_9b9075935c74707c: function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = getObject(arg0) instanceof Uint8Array;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_isSafeInteger_bfbc7332a9768d2a: function(arg0) {
|
||||||
|
const ret = Number.isSafeInteger(getObject(arg0));
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_length_32ed9a279acd054c: function(arg0) {
|
||||||
|
const ret = getObject(arg0).length;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_length_35a7bace40f36eac: function(arg0) {
|
||||||
|
const ret = getObject(arg0).length;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_new_361308b2356cecd0: function() {
|
||||||
|
const ret = new Object();
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_new_3eb36ae241fe6f44: function() {
|
||||||
|
const ret = new Array();
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_new_8a6f238a6ece86ea: function() {
|
||||||
|
const ret = new Error();
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_new_dd2b680c8bf6ae29: function(arg0) {
|
||||||
|
const ret = new Uint8Array(getObject(arg0));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbg_prototypesetcall_bdcdcc5842e4d77d: function(arg0, arg1, arg2) {
|
||||||
|
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));
|
||||||
|
},
|
||||||
|
__wbg_set_3f1d0b984ed272ed: function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
||||||
|
},
|
||||||
|
__wbg_set_f43e577aea94465b: function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
|
||||||
|
},
|
||||||
|
__wbg_stack_0ed75d68575b0f3c: function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg1).stack;
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbindgen_cast_0000000000000001: function(arg0) {
|
||||||
|
// Cast intrinsic for `F64 -> Externref`.
|
||||||
|
const ret = arg0;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||||
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
||||||
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbindgen_cast_0000000000000003: function(arg0) {
|
||||||
|
// Cast intrinsic for `U64 -> Externref`.
|
||||||
|
const ret = BigInt.asUintN(64, arg0);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbindgen_object_clone_ref: function(arg0) {
|
||||||
|
const ret = getObject(arg0);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
},
|
||||||
|
__wbindgen_object_drop_ref: function(arg0) {
|
||||||
|
takeObject(arg0);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
__proto__: null,
|
||||||
|
"./pi_natives_bg.js": import0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CompiledPatternFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(ptr => wasm.__wbg_compiledpattern_free(ptr >>> 0, 1));
|
||||||
|
const PhotonImageFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(ptr => wasm.__wbg_photonimage_free(ptr >>> 0, 1));
|
||||||
|
|
||||||
|
function addHeapObject(obj) {
|
||||||
|
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||||
|
const idx = heap_next;
|
||||||
|
heap_next = heap[idx];
|
||||||
|
|
||||||
|
heap[idx] = obj;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _assertClass(instance, klass) {
|
||||||
|
if (!(instance instanceof klass)) {
|
||||||
|
throw new Error(`expected instance of ${klass.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugString(val) {
|
||||||
|
// primitive types
|
||||||
|
const type = typeof val;
|
||||||
|
if (type == 'number' || type == 'boolean' || val == null) {
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
if (type == 'string') {
|
||||||
|
return `"${val}"`;
|
||||||
|
}
|
||||||
|
if (type == 'symbol') {
|
||||||
|
const description = val.description;
|
||||||
|
if (description == null) {
|
||||||
|
return 'Symbol';
|
||||||
|
} else {
|
||||||
|
return `Symbol(${description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 'function') {
|
||||||
|
const name = val.name;
|
||||||
|
if (typeof name == 'string' && name.length > 0) {
|
||||||
|
return `Function(${name})`;
|
||||||
|
} else {
|
||||||
|
return 'Function';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// objects
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const length = val.length;
|
||||||
|
let debug = '[';
|
||||||
|
if (length > 0) {
|
||||||
|
debug += debugString(val[0]);
|
||||||
|
}
|
||||||
|
for(let i = 1; i < length; i++) {
|
||||||
|
debug += ', ' + debugString(val[i]);
|
||||||
|
}
|
||||||
|
debug += ']';
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
// Test for built-in
|
||||||
|
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||||
|
let className;
|
||||||
|
if (builtInMatches && builtInMatches.length > 1) {
|
||||||
|
className = builtInMatches[1];
|
||||||
|
} else {
|
||||||
|
// Failed to match the standard '[object ClassName]'
|
||||||
|
return toString.call(val);
|
||||||
|
}
|
||||||
|
if (className == 'Object') {
|
||||||
|
// we're a user defined class or Object
|
||||||
|
// JSON.stringify avoids problems with cycles, and is generally much
|
||||||
|
// easier than looping through ownProperties of `val`.
|
||||||
|
try {
|
||||||
|
return 'Object(' + JSON.stringify(val) + ')';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// errors
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||||
|
}
|
||||||
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropObject(idx) {
|
||||||
|
if (idx < 132) return;
|
||||||
|
heap[idx] = heap_next;
|
||||||
|
heap_next = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayU8FromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedDataViewMemory0 = null;
|
||||||
|
function getDataViewMemory0() {
|
||||||
|
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||||
|
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedDataViewMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return decodeText(ptr, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
function getUint8ArrayMemory0() {
|
||||||
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||||
|
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedUint8ArrayMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObject(idx) { return heap[idx]; }
|
||||||
|
|
||||||
|
let heap = new Array(128).fill(undefined);
|
||||||
|
heap.push(undefined, null, true, false);
|
||||||
|
|
||||||
|
let heap_next = heap.length;
|
||||||
|
|
||||||
|
function isLikeNone(x) {
|
||||||
|
return x === undefined || x === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passArray8ToWasm0(arg, malloc) {
|
||||||
|
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||||
|
WASM_VECTOR_LEN = arg.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len, 1) >>> 0;
|
||||||
|
|
||||||
|
const mem = getUint8ArrayMemory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||||
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeObject(idx) {
|
||||||
|
const ret = getObject(idx);
|
||||||
|
dropObject(idx);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
function decodeText(ptr, len) {
|
||||||
|
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextEncoder = new TextEncoder();
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
const wasmUrl = new URL('pi_natives_bg.wasm', import.meta.url);
|
||||||
|
const wasmInstantiated = await WebAssembly.instantiateStreaming(fetch(wasmUrl), __wbg_get_imports());
|
||||||
|
const wasm = wasmInstantiated.instance.exports;
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f1112971f9faa2106c20a5428f9d01684e6b35810482ff0878e65dd413fc6b91
|
||||||
|
size 1897187
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const __wbg_compiledpattern_free: (a: number, b: number) => void;
|
||||||
|
export const __wbg_photonimage_free: (a: number, b: number) => void;
|
||||||
|
export const compiledpattern_has_match: (a: number, b: number, c: number) => number;
|
||||||
|
export const compiledpattern_new: (a: number, b: number) => void;
|
||||||
|
export const compiledpattern_search: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||||
|
export const has_match: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
|
||||||
|
export const photonimage_get_bytes: (a: number, b: number) => void;
|
||||||
|
export const photonimage_get_bytes_jpeg: (a: number, b: number, c: number) => void;
|
||||||
|
export const photonimage_get_height: (a: number) => number;
|
||||||
|
export const photonimage_get_width: (a: number) => number;
|
||||||
|
export const photonimage_new_from_byteslice: (a: number, b: number, c: number) => void;
|
||||||
|
export const resize: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
export const search: (a: number, b: number, c: number) => number;
|
||||||
|
export const compiledpattern_has_match_bytes: (a: number, b: number, c: number) => number;
|
||||||
|
export const compiledpattern_search_bytes: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||||
|
export const __wbindgen_export: (a: number, b: number) => number;
|
||||||
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
export const __wbindgen_export3: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"name": "pi-mono",
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[imports]
|
||||||
|
granularity = { enforce = true, group = "crate" }
|
||||||
|
preferNoStd = false
|
||||||
|
preferPrelude = true
|
||||||
|
prefix = "crate"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||||
|
targets = ["wasm32-unknown-unknown"]
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Edition
|
||||||
|
edition = "2024"
|
||||||
|
style_edition = "2024"
|
||||||
|
|
||||||
|
# Import Organization
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
imports_indent = "Block"
|
||||||
|
reorder_imports = true
|
||||||
|
|
||||||
|
# Comments & Documentation
|
||||||
|
comment_width = 80
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
normalize_doc_attributes = true
|
||||||
|
wrap_comments = true
|
||||||
|
|
||||||
|
# Width & Length
|
||||||
|
chain_width = 60
|
||||||
|
max_width = 100
|
||||||
|
single_line_if_else_max_width = 50
|
||||||
|
single_line_let_else_max_width = 50
|
||||||
|
|
||||||
|
# Indentation
|
||||||
|
hard_tabs = true
|
||||||
|
indent_style = "Block"
|
||||||
|
tab_spaces = 3
|
||||||
|
|
||||||
|
# Spacing
|
||||||
|
binop_separator = "Front"
|
||||||
|
space_after_colon = true
|
||||||
|
space_before_colon = false
|
||||||
|
spaces_around_ranges = false
|
||||||
|
|
||||||
|
# Trailing Delimiters
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
trailing_comma = "Vertical"
|
||||||
|
trailing_semicolon = true
|
||||||
|
|
||||||
|
# Formatting Behavior
|
||||||
|
condense_wildcard_suffixes = true
|
||||||
|
merge_derives = true
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
remove_nested_parens = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_small_heuristics = "Max"
|
||||||
|
|
||||||
|
# Macros & Strings
|
||||||
|
format_macro_bodies = true
|
||||||
|
format_macro_matchers = true
|
||||||
|
format_strings = true
|
||||||
|
|
||||||
|
# Code Structure
|
||||||
|
brace_style = "SameLineWhere"
|
||||||
|
fn_params_layout = "Tall"
|
||||||
|
reorder_impl_items = true
|
||||||
|
where_single_line = false
|
||||||
|
|
||||||
|
# Match Expressions
|
||||||
|
match_arm_blocks = true
|
||||||
|
match_arm_leading_pipes = "Never"
|
||||||
|
|
||||||
|
# Alignment
|
||||||
|
enum_discrim_align_threshold = 20
|
||||||
|
struct_field_align_threshold = 20
|
||||||
|
|
||||||
|
# Blank Lines
|
||||||
|
blank_lines_lower_bound = 0
|
||||||
|
blank_lines_upper_bound = 1
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
force_explicit_abi = true
|
||||||
|
hex_literal_case = "Lower"
|
||||||
|
newline_style = "Unix"
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
"@oh-my-pi/pi-coding-agent": ["./packages/coding-agent/src/index.ts"],
|
"@oh-my-pi/pi-coding-agent": ["./packages/coding-agent/src/index.ts"],
|
||||||
"@oh-my-pi/pi-coding-agent/hooks": ["./packages/coding-agent/src/extensibility/hooks/index.ts"],
|
"@oh-my-pi/pi-coding-agent/hooks": ["./packages/coding-agent/src/extensibility/hooks/index.ts"],
|
||||||
"@oh-my-pi/pi-coding-agent/*": ["./packages/coding-agent/src/*"],
|
"@oh-my-pi/pi-coding-agent/*": ["./packages/coding-agent/src/*"],
|
||||||
|
"@oh-my-pi/pi-natives": ["./packages/natives/src/index.ts"],
|
||||||
|
"@oh-my-pi/pi-natives/*": ["./packages/natives/src/*"],
|
||||||
"@oh-my-pi/omp-stats": ["./packages/stats/src/index.ts"],
|
"@oh-my-pi/omp-stats": ["./packages/stats/src/index.ts"],
|
||||||
"@oh-my-pi/omp-stats/src/*": ["./packages/stats/src/*"],
|
"@oh-my-pi/omp-stats/src/*": ["./packages/stats/src/*"],
|
||||||
"@oh-my-pi/omp-stats/*": ["./packages/stats/src/*"],
|
"@oh-my-pi/omp-stats/*": ["./packages/stats/src/*"],
|
||||||
|
|||||||
Reference in New Issue
Block a user