Files
oh-my-pi/docs/natives-binding-contract.md
T
2026-04-30 06:47:01 +02:00

135 lines
9.7 KiB
Markdown

# Natives Binding Contract (JavaScript/TypeScript Side)
This document defines the JS/TS contract between `@oh-my-pi/pi-natives` callers and the loaded N-API addon.
Current package shape is direct-to-native: there is no `packages/natives/src/<module>` TypeScript wrapper layer. The public API is the generated `packages/natives/native/index.d.ts` declaration file, the CommonJS loader in `packages/natives/native/index.js`, and the Rust `#[napi]` exports in `crates/pi-natives/src`.
## Implementation files
- `packages/natives/native/index.js`
- `packages/natives/native/index.d.ts`
- `packages/natives/native/loader-state.js`
- `packages/natives/scripts/build-native.ts`
- `packages/natives/scripts/gen-enums.ts`
- `packages/natives/package.json`
- `crates/pi-natives/src/lib.rs`
- Rust modules under `crates/pi-natives/src/*.rs`
## Contract model
The contract has three parts:
1. **Generated runtime loader** (`native/index.js`)
- computes candidates and `require(...)`s the `.node` addon;
- exports the loaded addon object directly;
- appends enum objects generated by `scripts/gen-enums.ts`.
2. **Generated TypeScript declarations** (`native/index.d.ts`)
- generated by napi-rs during `scripts/build-native.ts`;
- declares exported functions, classes, object interfaces, and native enums;
- is the package `types` entry.
3. **Rust N-API exports** (`crates/pi-natives/src`)
- `#[napi]` functions/classes/objects/enums are the source of generated declarations and runtime symbols;
- snake_case Rust names become camelCase JavaScript names by napi-rs convention.
There is no current `NativeBindings` declaration-merging lifecycle and no `validateNative(...)` required-export list in the loader.
## Public export surface organization
`packages/natives/package.json` exposes the package root only:
```json
{
"main": "./native/index.js",
"types": "./native/index.d.ts",
"exports": {
".": {
"types": "./native/index.d.ts",
"import": "./native/index.js"
}
}
}
```
Consumers in `packages/coding-agent` and `packages/tui` import directly from `@oh-my-pi/pi-natives`.
## JS API ↔ native export mapping (representative)
| Category | Public JS API | Rust source | Return style |
| ----------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------- |
| Grep | `grep(options, onMatch?)` | `grep.rs` | `Promise<GrepResult>` |
| Grep | `search(content, options)` | `grep.rs` | `SearchResult` |
| Grep | `hasMatch(content, pattern, ignoreCase?, multiline?)` | `grep.rs` | `boolean` |
| Fuzzy path search | `fuzzyFind(options)` | `fd.rs` | `Promise<FuzzyFindResult>` |
| Glob | `glob(options, onMatch?)` | `glob.rs` | `Promise<GlobResult>` |
| Glob cache | `invalidateFsScanCache(path?)` | `fs_cache.rs` | `void` |
| AST search/edit | `astGrep(options)`, `astEdit(options)` | `ast.rs` | `Promise<...>` |
| Shell | `executeShell(options, onChunk?)` | `shell.rs` | `Promise<ShellExecuteResult>` |
| Shell | `new Shell(options?)`, `shell.run(...)`, `shell.abort()` | `shell.rs` | class / promises |
| PTY | `new PtySession()`, `start/write/resize/kill` | `pty.rs` | class / promises |
| Process | `killTree(pid, signal)`, `listDescendants(pid)` | `ps.rs` | sync |
| Keys | `parseKey`, `matchesKey`, Kitty/legacy helpers | `keys.rs` | sync |
| Text | `wrapTextWithAnsi`, `truncateToWidth`, `sliceWithWidth`, `extractSegments`, `sanitizeText`, `visibleWidth` | `text.rs` | sync |
| Highlight | `highlightCode`, `supportsLanguage`, `getSupportedLanguages` | `highlight.rs` | sync |
| HTML | `htmlToMarkdown(html, options?)` | `html.rs` | `Promise<string>` |
| Image | `PhotonImage`, `encodeSixel` | `image.rs` | class / sync / promises |
| Clipboard | `copyToClipboard`, `readImageFromClipboard` | `clipboard.rs` | sync / promise |
| Tokens | `countTokens(input, encoding?)` | `tokens.rs` | sync |
| System | `detectMacOSAppearance`, `MacAppearanceObserver`, `MacOSPowerAssertion`, `getWorkProfile`, ProjFS helpers | `appearance.rs`, `power.rs`, `prof.rs`, `projfs_overlay.rs` | mixed |
## Sync vs async contract differences
The contract preserves Rust/N-API call style:
- **Promise-returning exports** for worker-thread or async runtime work (`grep`, `glob`, `fuzzyFind`, `astGrep`, `astEdit`, `htmlToMarkdown`, shell/PTY runs, image parse/resize/encode, clipboard image read).
- **Synchronous exports** for deterministic in-memory transforms/parsers or direct system calls (`search`, `hasMatch`, highlighting, text utilities, token counting, process queries, `copyToClipboard`, `encodeSixel`).
- **Constructor exports** for stateful runtime objects (`Shell`, `PtySession`, `PhotonImage`, macOS observer/power handles).
Changing sync ↔ async for an existing export is a breaking public API change because consumers call these exports directly.
## Object and enum typing patterns
### Object patterns
`#[napi(object)]` Rust structs become TS interfaces, for example:
- `GrepResult`, `SearchResult`, `GlobResult`, `FuzzyFindResult`
- `ShellRunResult`, `ShellExecuteResult`, `PtyRunResult`, `MinimizerResult`
- `AstFindResult`, `AstReplaceResult`
- `System`/media payloads such as `ClipboardImage`, `WorkProfile`, `ParsedKittyResult`
Runtime shape correctness is owned by napi-rs and the Rust implementation.
### Enum patterns
Native enums are represented in generated declarations and also appended to `module.exports` by `scripts/gen-enums.ts`, because the loader is hand-maintained CommonJS around the generated addon. Current enum objects include:
- `AstMatchStrictness`
- `Ellipsis`
- `Encoding`
- `FileType`
- `GrepOutputMode`
- `ImageFormat`
- `KeyEventType`
- `MacOSAppearance`
- `SamplingFilter`
## Error behavior and caveats
- Addon load failure or unsupported platform throws during package import from `native/index.js`.
- The loader does not verify the full export set after `require(...)`; stale or mismatched binaries surface as native load errors or missing members at use sites.
- N-API conversion validates basic argument conversion, but TS optional fields do not guarantee semantic validity for untyped callers.
- Numeric enum declarations do not prevent out-of-range numeric values from untyped callers unless the Rust function rejects them during conversion.
- Callback exports use napi-rs `ThreadsafeFunction` shape: `(error: Error | null, value) => void`. Native code generally emits successful values; hard failures reject/throw through the owning call.
## Maintainer checklist for binding changes
When adding/changing an export, update all of:
1. Rust `#[napi]` implementation in the owning `crates/pi-natives/src/<module>.rs`.
2. `crates/pi-natives/src/lib.rs` if a new module is added.
3. Any consumer imports/callsites in `packages/coding-agent` or `packages/tui`.
4. Build output by running the natives build so `native/index.d.ts` and `native/index.js` stay in sync.
5. `scripts/gen-enums.ts` if enum runtime export patching needs to change.
Do not add a parallel TS wrapper convention unless the package design intentionally moves back to wrappers; current consumers depend on the direct generated API.