@oh-my-pi/pi-natives 9.4.0 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -13
- package/native/pi_natives.darwin-arm64.node +0 -0
- package/native/pi_natives.darwin-x64.node +0 -0
- package/native/pi_natives.linux-arm64.node +0 -0
- package/native/pi_natives.linux-x64.node +0 -0
- package/native/pi_natives.win32-x64.node +0 -0
- package/package.json +6 -6
- package/src/find/types.ts +31 -0
- package/src/grep/index.ts +37 -295
- package/src/grep/types.ts +59 -19
- package/src/highlight/index.ts +5 -13
- package/src/html/index.ts +5 -35
- package/src/html/types.ts +1 -16
- package/src/image/index.ts +52 -73
- package/src/index.ts +26 -94
- package/src/keys/index.ts +10 -0
- package/src/native.ts +159 -0
- package/src/request-options.ts +94 -0
- package/src/text/index.ts +14 -27
- package/src/grep/file-reader.ts +0 -51
- package/src/grep/filters.ts +0 -77
- package/src/grep/worker.ts +0 -212
- package/src/html/worker.ts +0 -40
- package/src/image/types.ts +0 -52
- package/src/image/worker.ts +0 -152
- package/src/pool.ts +0 -362
- package/src/wasix.ts +0 -1745
- package/src/worker-resolver.ts +0 -9
- package/wasm/pi_natives.d.ts +0 -148
- package/wasm/pi_natives.js +0 -891
- package/wasm/pi_natives_bg.wasm +0 -0
- package/wasm/pi_natives_bg.wasm.d.ts +0 -32
package/src/html/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Types for HTML to Markdown
|
|
2
|
+
* Types for HTML to Markdown conversion.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export interface HtmlToMarkdownOptions {
|
|
@@ -8,18 +8,3 @@ export interface HtmlToMarkdownOptions {
|
|
|
8
8
|
/** Skip images during conversion */
|
|
9
9
|
skipImages?: boolean;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
export type HtmlRequest =
|
|
13
|
-
| { type: "init"; id: number }
|
|
14
|
-
| { type: "destroy" }
|
|
15
|
-
| {
|
|
16
|
-
type: "convert";
|
|
17
|
-
id: number;
|
|
18
|
-
html: string;
|
|
19
|
-
options?: HtmlToMarkdownOptions;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type HtmlResponse =
|
|
23
|
-
| { type: "ready"; id: number }
|
|
24
|
-
| { type: "error"; id: number; error: string }
|
|
25
|
-
| { type: "converted"; id: number; markdown: string };
|
package/src/image/index.ts
CHANGED
|
@@ -1,48 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* All heavy lifting happens in a worker thread to avoid blocking the main thread.
|
|
5
|
-
* Uses transferable ArrayBuffers to avoid copying image data.
|
|
2
|
+
* Image processing via native bindings.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
5
|
+
import { type NativePhotonImage, native } from "../native";
|
|
6
|
+
|
|
7
|
+
const images = new Map<number, NativePhotonImage>();
|
|
8
|
+
let nextHandle = 1;
|
|
9
|
+
|
|
10
|
+
function registerImage(image: NativePhotonImage): number {
|
|
11
|
+
const handle = nextHandle++;
|
|
12
|
+
images.set(handle, image);
|
|
13
|
+
return handle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getImage(handle: number): NativePhotonImage {
|
|
17
|
+
const image = images.get(handle);
|
|
18
|
+
if (!image) {
|
|
19
|
+
throw new Error("Image already freed");
|
|
20
|
+
}
|
|
21
|
+
return image;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SamplingFilter = native.SamplingFilter;
|
|
25
|
+
export type SamplingFilter = (typeof SamplingFilter)[keyof typeof SamplingFilter];
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Image handle for async operations.
|
|
29
|
-
* Must call free() when done to release WASM memory.
|
|
30
29
|
*/
|
|
31
30
|
export class PhotonImage {
|
|
32
31
|
#handle: number;
|
|
33
|
-
#width: number;
|
|
34
|
-
#height: number;
|
|
35
32
|
#freed = false;
|
|
36
33
|
|
|
37
|
-
private constructor(handle: number
|
|
34
|
+
private constructor(handle: number) {
|
|
38
35
|
this.#handle = handle;
|
|
39
|
-
this.#width = width;
|
|
40
|
-
this.#height = height;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
/** @internal */
|
|
44
|
-
static _create(handle: number
|
|
45
|
-
|
|
39
|
+
static _create(handle: number): PhotonImage {
|
|
40
|
+
if (!images.has(handle)) {
|
|
41
|
+
throw new Error("Invalid image handle");
|
|
42
|
+
}
|
|
43
|
+
return new PhotonImage(handle);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load an image from encoded bytes (PNG, JPEG, WebP, GIF).
|
|
48
|
+
*/
|
|
49
|
+
static async new_from_byteslice(bytes: Uint8Array): Promise<PhotonImage> {
|
|
50
|
+
const image = await native.PhotonImage.newFromByteslice(bytes);
|
|
51
|
+
const handle = registerImage(image);
|
|
52
|
+
return new PhotonImage(handle);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/** @internal */
|
|
@@ -51,56 +58,36 @@ export class PhotonImage {
|
|
|
51
58
|
return this.#handle;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
static async new_from_byteslice(bytes: Uint8Array): Promise<PhotonImage> {
|
|
59
|
-
const response = await pool.request<Extract<ImageResponse, { type: "loaded" }>>(
|
|
60
|
-
{ type: "load", bytes },
|
|
61
|
-
{
|
|
62
|
-
transfer: [bytes.buffer],
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
return new PhotonImage(response.handle, response.width, response.height);
|
|
61
|
+
#native(): NativePhotonImage {
|
|
62
|
+
if (this.#freed) throw new Error("Image already freed");
|
|
63
|
+
return getImage(this.#handle);
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
/** Get image width in pixels. */
|
|
69
67
|
get_width(): number {
|
|
70
|
-
return this.#
|
|
68
|
+
return this.#native().getWidth();
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
/** Get image height in pixels. */
|
|
74
72
|
get_height(): number {
|
|
75
|
-
return this.#
|
|
73
|
+
return this.#native().getHeight();
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
/** Export as PNG bytes. */
|
|
79
77
|
async get_bytes(): Promise<Uint8Array> {
|
|
80
|
-
|
|
81
|
-
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
|
82
|
-
type: "get_png",
|
|
83
|
-
handle: this.#handle,
|
|
84
|
-
});
|
|
85
|
-
return response.bytes;
|
|
78
|
+
return this.#native().getBytes();
|
|
86
79
|
}
|
|
87
80
|
|
|
88
81
|
/** Export as JPEG bytes with specified quality (0-100). */
|
|
89
82
|
async get_bytes_jpeg(quality: number): Promise<Uint8Array> {
|
|
90
|
-
|
|
91
|
-
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
|
92
|
-
type: "get_jpeg",
|
|
93
|
-
handle: this.#handle,
|
|
94
|
-
quality,
|
|
95
|
-
});
|
|
96
|
-
return response.bytes;
|
|
83
|
+
return this.#native().getBytesJpeg(quality);
|
|
97
84
|
}
|
|
98
85
|
|
|
99
|
-
/** Release
|
|
86
|
+
/** Release native resources. */
|
|
100
87
|
free() {
|
|
101
88
|
if (this.#freed) return;
|
|
102
89
|
this.#freed = true;
|
|
103
|
-
|
|
90
|
+
images.delete(this.#handle);
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
/** Alias for free() to support using-declarations. */
|
|
@@ -114,21 +101,13 @@ export class PhotonImage {
|
|
|
114
101
|
* Returns a new PhotonImage (original is not modified).
|
|
115
102
|
*/
|
|
116
103
|
export async function resize(image: PhotonImage, width: number, height: number, filter: number): Promise<PhotonImage> {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
width,
|
|
122
|
-
height,
|
|
123
|
-
filter,
|
|
124
|
-
});
|
|
125
|
-
return PhotonImage._create(response.handle, response.width, response.height);
|
|
104
|
+
const nativeImage = getImage(image._getHandle());
|
|
105
|
+
const resized = await nativeImage.resize(width, height, filter);
|
|
106
|
+
const handle = registerImage(resized);
|
|
107
|
+
return PhotonImage._create(handle);
|
|
126
108
|
}
|
|
127
109
|
|
|
128
110
|
/**
|
|
129
|
-
* Terminate
|
|
130
|
-
* Call this when shutting down to clean up resources.
|
|
111
|
+
* Terminate image resources (no-op for native bindings).
|
|
131
112
|
*/
|
|
132
|
-
export function terminate(): void {
|
|
133
|
-
pool.terminate();
|
|
134
|
-
}
|
|
113
|
+
export function terminate(): void {}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Native utilities powered by
|
|
2
|
+
* Native utilities powered by N-API.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import * as fs from "node:fs/promises";
|
|
6
5
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
6
|
+
import type { FindMatch, FindOptions, FindResult } from "./find/types";
|
|
7
|
+
import { native } from "./native";
|
|
8
8
|
|
|
9
9
|
// =============================================================================
|
|
10
10
|
// Grep (ripgrep-based regex search)
|
|
@@ -12,52 +12,24 @@ import { globPaths } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
|
|
13
13
|
export {
|
|
14
14
|
type ContextLine,
|
|
15
|
+
type FuzzyFindMatch,
|
|
16
|
+
type FuzzyFindOptions,
|
|
17
|
+
type FuzzyFindResult,
|
|
18
|
+
fuzzyFind,
|
|
15
19
|
type GrepMatch,
|
|
16
20
|
type GrepOptions,
|
|
17
21
|
type GrepResult,
|
|
18
22
|
type GrepSummary,
|
|
19
23
|
grep,
|
|
20
|
-
grepDirect,
|
|
21
|
-
grepPool,
|
|
22
24
|
hasMatch,
|
|
23
25
|
searchContent,
|
|
24
|
-
terminate,
|
|
25
26
|
} from "./grep/index";
|
|
26
27
|
|
|
27
|
-
// =============================================================================
|
|
28
|
-
// WASI implementation
|
|
29
|
-
// =============================================================================
|
|
30
|
-
|
|
31
|
-
export { WASI1, WASIError, WASIExitError, type WASIOptions } from "./wasix";
|
|
32
|
-
|
|
33
28
|
// =============================================================================
|
|
34
29
|
// Find (file discovery)
|
|
35
30
|
// =============================================================================
|
|
36
31
|
|
|
37
|
-
export
|
|
38
|
-
/** Glob pattern to match (e.g., `*.ts`) */
|
|
39
|
-
pattern: string;
|
|
40
|
-
/** Directory to search */
|
|
41
|
-
path: string;
|
|
42
|
-
/** Filter by file type: "file", "dir", or "symlink" */
|
|
43
|
-
fileType?: "file" | "dir" | "symlink";
|
|
44
|
-
/** Include hidden files (default: false) */
|
|
45
|
-
hidden?: boolean;
|
|
46
|
-
/** Maximum number of results */
|
|
47
|
-
maxResults?: number;
|
|
48
|
-
/** Respect .gitignore files (default: true) */
|
|
49
|
-
gitignore?: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface FindMatch {
|
|
53
|
-
path: string;
|
|
54
|
-
fileType: "file" | "dir" | "symlink";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface FindResult {
|
|
58
|
-
matches: FindMatch[];
|
|
59
|
-
totalMatches: number;
|
|
60
|
-
}
|
|
32
|
+
export type { FindMatch, FindOptions, FindResult } from "./find/types";
|
|
61
33
|
|
|
62
34
|
/**
|
|
63
35
|
* Find files matching a glob pattern.
|
|
@@ -70,59 +42,19 @@ export async function find(options: FindOptions, onMatch?: (match: FindMatch) =>
|
|
|
70
42
|
// Convert simple patterns to recursive globs if needed
|
|
71
43
|
const globPattern = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
|
|
72
44
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (const p of paths) {
|
|
84
|
-
if (matches.length >= maxResults) {
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const normalizedPath = p.replace(/\\/g, "/");
|
|
89
|
-
if (!normalizedPath) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let stats: Awaited<ReturnType<typeof fs.lstat>>;
|
|
94
|
-
try {
|
|
95
|
-
stats = await fs.lstat(path.join(searchPath, normalizedPath));
|
|
96
|
-
} catch {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const fileType: "file" | "dir" | "symlink" = stats.isSymbolicLink()
|
|
101
|
-
? "symlink"
|
|
102
|
-
: stats.isDirectory()
|
|
103
|
-
? "dir"
|
|
104
|
-
: "file";
|
|
105
|
-
|
|
106
|
-
if (options.fileType && options.fileType !== fileType) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const match: FindMatch = {
|
|
111
|
-
path: normalizedPath,
|
|
112
|
-
fileType,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
matches.push(match);
|
|
116
|
-
onMatch?.(match);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
matches,
|
|
121
|
-
totalMatches: matches.length,
|
|
122
|
-
};
|
|
45
|
+
return native.find(
|
|
46
|
+
{
|
|
47
|
+
...options,
|
|
48
|
+
path: searchPath,
|
|
49
|
+
pattern: globPattern,
|
|
50
|
+
hidden: options.hidden ?? false,
|
|
51
|
+
gitignore: options.gitignore ?? true,
|
|
52
|
+
},
|
|
53
|
+
onMatch,
|
|
54
|
+
);
|
|
123
55
|
}
|
|
124
56
|
|
|
125
|
-
//
|
|
57
|
+
// ===================================================== ========================
|
|
126
58
|
// Image processing (photon-compatible API)
|
|
127
59
|
// =============================================================================
|
|
128
60
|
|
|
@@ -143,7 +75,6 @@ export {
|
|
|
143
75
|
type SliceWithWidthResult,
|
|
144
76
|
sliceWithWidth,
|
|
145
77
|
truncateToWidth,
|
|
146
|
-
visibleWidth,
|
|
147
78
|
} from "./text/index";
|
|
148
79
|
|
|
149
80
|
// =============================================================================
|
|
@@ -157,6 +88,12 @@ export {
|
|
|
157
88
|
supportsLanguage,
|
|
158
89
|
} from "./highlight/index";
|
|
159
90
|
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// Keyboard sequence helpers
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
export { matchesKittySequence } from "./keys/index";
|
|
96
|
+
|
|
160
97
|
// =============================================================================
|
|
161
98
|
// HTML to Markdown
|
|
162
99
|
// =============================================================================
|
|
@@ -164,11 +101,6 @@ export {
|
|
|
164
101
|
export {
|
|
165
102
|
type HtmlToMarkdownOptions,
|
|
166
103
|
htmlToMarkdown,
|
|
167
|
-
terminate as terminateHtmlWorker,
|
|
168
104
|
} from "./html/index";
|
|
169
105
|
|
|
170
|
-
|
|
171
|
-
// Worker Pool (shared infrastructure)
|
|
172
|
-
// =============================================================================
|
|
173
|
-
|
|
174
|
-
export { type BaseRequest, type BaseResponse, type RequestOptions, WorkerPool, type WorkerPoolOptions } from "./pool";
|
|
106
|
+
export type { RequestOptions } from "./request-options";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard sequence utilities powered by native bindings.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { native } from "../native";
|
|
6
|
+
|
|
7
|
+
/** Match Kitty protocol sequences for codepoint and modifier. */
|
|
8
|
+
export function matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean {
|
|
9
|
+
return native.matchesKittySequence(data, expectedCodepoint, expectedModifier);
|
|
10
|
+
}
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { FindMatch, FindOptions, FindResult } from "./find/types";
|
|
4
|
+
import type {
|
|
5
|
+
FuzzyFindOptions,
|
|
6
|
+
FuzzyFindResult,
|
|
7
|
+
GrepOptions,
|
|
8
|
+
GrepResult,
|
|
9
|
+
SearchOptions,
|
|
10
|
+
SearchResult,
|
|
11
|
+
} from "./grep/types";
|
|
12
|
+
import type { HighlightColors } from "./highlight/index";
|
|
13
|
+
import type { HtmlToMarkdownOptions } from "./html/types";
|
|
14
|
+
import type { ExtractSegmentsResult, SliceWithWidthResult, TextInput } from "./text/index";
|
|
15
|
+
|
|
16
|
+
export interface NativePhotonImage {
|
|
17
|
+
getWidth(): number;
|
|
18
|
+
getHeight(): number;
|
|
19
|
+
getBytes(): Promise<Uint8Array>;
|
|
20
|
+
getBytesJpeg(quality: number): Promise<Uint8Array>;
|
|
21
|
+
getBytesWebp(): Promise<Uint8Array>;
|
|
22
|
+
getBytesGif(): Promise<Uint8Array>;
|
|
23
|
+
resize(width: number, height: number, filter: number): Promise<NativePhotonImage>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NativePhotonImageConstructor {
|
|
27
|
+
newFromByteslice(bytes: Uint8Array): Promise<NativePhotonImage>;
|
|
28
|
+
prototype: NativePhotonImage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NativeSamplingFilter {
|
|
32
|
+
Nearest: 1;
|
|
33
|
+
Triangle: 2;
|
|
34
|
+
CatmullRom: 3;
|
|
35
|
+
Gaussian: 4;
|
|
36
|
+
Lanczos3: 5;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
import type { GrepMatch } from "./grep/types";
|
|
40
|
+
|
|
41
|
+
export interface NativeBindings {
|
|
42
|
+
find(options: FindOptions, onMatch?: (match: FindMatch) => void): Promise<FindResult>;
|
|
43
|
+
fuzzyFind(options: FuzzyFindOptions): Promise<FuzzyFindResult>;
|
|
44
|
+
grep(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult>;
|
|
45
|
+
search(content: string | Uint8Array, options: SearchOptions): SearchResult;
|
|
46
|
+
hasMatch(
|
|
47
|
+
content: string | Uint8Array,
|
|
48
|
+
pattern: string | Uint8Array,
|
|
49
|
+
ignoreCase: boolean,
|
|
50
|
+
multiline: boolean,
|
|
51
|
+
): boolean;
|
|
52
|
+
htmlToMarkdown(html: string, options?: HtmlToMarkdownOptions | null): Promise<string>;
|
|
53
|
+
highlightCode(code: string, lang: string | null | undefined, colors: HighlightColors): string;
|
|
54
|
+
supportsLanguage(lang: string): boolean;
|
|
55
|
+
getSupportedLanguages(): string[];
|
|
56
|
+
SamplingFilter: NativeSamplingFilter;
|
|
57
|
+
PhotonImage: NativePhotonImageConstructor;
|
|
58
|
+
truncateToWidth(text: TextInput, maxWidth: number, ellipsis: TextInput, pad: boolean): string;
|
|
59
|
+
sliceWithWidth(line: TextInput, startCol: number, length: number, strict: boolean): SliceWithWidthResult;
|
|
60
|
+
extractSegments(
|
|
61
|
+
line: TextInput,
|
|
62
|
+
beforeEnd: number,
|
|
63
|
+
afterStart: number,
|
|
64
|
+
afterLen: number,
|
|
65
|
+
strictAfter: boolean,
|
|
66
|
+
): ExtractSegmentsResult;
|
|
67
|
+
matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const require = createRequire(import.meta.url);
|
|
71
|
+
const platformTag = `${process.platform}-${process.arch}`;
|
|
72
|
+
const nativeDir = path.join(import.meta.dir, "..", "native");
|
|
73
|
+
const repoRoot = path.join(import.meta.dir, "..", "..", "..");
|
|
74
|
+
const execDir = path.dirname(process.execPath);
|
|
75
|
+
|
|
76
|
+
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
77
|
+
|
|
78
|
+
const candidates = [
|
|
79
|
+
path.join(nativeDir, `pi_natives.${platformTag}.node`),
|
|
80
|
+
path.join(nativeDir, "pi_natives.node"),
|
|
81
|
+
path.join(execDir, `pi_natives.${platformTag}.node`),
|
|
82
|
+
path.join(execDir, "pi_natives.node"),
|
|
83
|
+
path.join(repoRoot, "target", "release", "pi_natives.node"),
|
|
84
|
+
path.join(repoRoot, "crates", "pi-natives", "target", "release", "pi_natives.node"),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function loadNative(): NativeBindings {
|
|
88
|
+
const errors: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
try {
|
|
92
|
+
const bindings = require(candidate) as NativeBindings;
|
|
93
|
+
validateNative(bindings, candidate);
|
|
94
|
+
return bindings;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
errors.push(`${candidate}: ${message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if this is an unsupported platform
|
|
102
|
+
if (!SUPPORTED_PLATFORMS.includes(platformTag)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Unsupported platform: ${platformTag}\n` +
|
|
105
|
+
`Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
|
|
106
|
+
"If you need support for this platform, please open an issue.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const details = errors.map(error => `- ${error}`).join("\n");
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Failed to load pi_natives native addon for ${platformTag}.\n\n` +
|
|
113
|
+
`Tried:\n${details}\n\n` +
|
|
114
|
+
"If installed via npm/bun, try reinstalling: bun install @oh-my-pi/pi-natives\n" +
|
|
115
|
+
"If developing locally, build with: bun --cwd=packages/natives run build:native",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateNative(bindings: NativeBindings, source: string): void {
|
|
120
|
+
const missing: string[] = [];
|
|
121
|
+
const checkFn = (name: keyof NativeBindings) => {
|
|
122
|
+
if (typeof bindings[name] !== "function") {
|
|
123
|
+
missing.push(name);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
checkFn("find");
|
|
128
|
+
checkFn("fuzzyFind");
|
|
129
|
+
checkFn("grep");
|
|
130
|
+
checkFn("search");
|
|
131
|
+
checkFn("hasMatch");
|
|
132
|
+
checkFn("htmlToMarkdown");
|
|
133
|
+
checkFn("highlightCode");
|
|
134
|
+
checkFn("supportsLanguage");
|
|
135
|
+
checkFn("getSupportedLanguages");
|
|
136
|
+
checkFn("truncateToWidth");
|
|
137
|
+
checkFn("sliceWithWidth");
|
|
138
|
+
checkFn("extractSegments");
|
|
139
|
+
checkFn("matchesKittySequence");
|
|
140
|
+
|
|
141
|
+
if (!bindings.PhotonImage?.newFromByteslice) {
|
|
142
|
+
missing.push("PhotonImage.newFromByteslice");
|
|
143
|
+
}
|
|
144
|
+
if (!bindings.PhotonImage?.prototype?.resize) {
|
|
145
|
+
missing.push("PhotonImage.resize");
|
|
146
|
+
}
|
|
147
|
+
if (!bindings.SamplingFilter || typeof bindings.SamplingFilter.Lanczos3 !== "number") {
|
|
148
|
+
missing.push("SamplingFilter");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (missing.length) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Native addon missing exports (${source}). Missing: ${missing.join(", ")}. ` +
|
|
154
|
+
"Rebuild with `bun --cwd=packages/natives run build:native`.",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const native = loadNative();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface RequestOptions {
|
|
2
|
+
timeoutMs?: number;
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function abortReason(signal?: AbortSignal): Error {
|
|
7
|
+
if (!signal || !signal.reason) return new Error("Request aborted");
|
|
8
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
9
|
+
return new Error("Request aborted", { cause: signal.reason });
|
|
10
|
+
}
|
|
11
|
+
export async function wrapRequestOptions<T>(fn: () => Promise<T>, options?: RequestOptions): Promise<T> {
|
|
12
|
+
const timeoutMs = options?.timeoutMs ?? 0;
|
|
13
|
+
const signal = options?.signal;
|
|
14
|
+
|
|
15
|
+
// Fast path: no timeout + no signal
|
|
16
|
+
if (!signal && timeoutMs <= 0) return fn();
|
|
17
|
+
|
|
18
|
+
// If already aborted, fail immediately
|
|
19
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
20
|
+
|
|
21
|
+
// If we only have an abort signal and no timeout, keep it simple.
|
|
22
|
+
if (signal && timeoutMs <= 0) {
|
|
23
|
+
return withAbortSignal(fn, signal);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return withTimeoutAndOptionalAbort(fn, timeoutMs, signal);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withAbortSignal<T>(fn: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
30
|
+
return new Promise<T>((resolve, reject) => {
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
signal.removeEventListener("abort", onAbort);
|
|
33
|
+
reject(abortReason(signal));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
37
|
+
|
|
38
|
+
// If it races and aborts right after addEventListener, `once` handles it,
|
|
39
|
+
// but we still want to short-circuit.
|
|
40
|
+
if (signal.aborted) return onAbort();
|
|
41
|
+
|
|
42
|
+
fn().then(
|
|
43
|
+
v => {
|
|
44
|
+
signal.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve(v);
|
|
46
|
+
},
|
|
47
|
+
err => {
|
|
48
|
+
signal.removeEventListener("abort", onAbort);
|
|
49
|
+
reject(err);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function withTimeoutAndOptionalAbort<T>(fn: () => Promise<T>, timeoutMs: number, signal?: AbortSignal): Promise<T> {
|
|
56
|
+
return new Promise<T>((resolve, reject) => {
|
|
57
|
+
let settled = false;
|
|
58
|
+
|
|
59
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
if (timeoutId) {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
timeoutId = undefined;
|
|
64
|
+
}
|
|
65
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const settle = (ok: boolean, value: any) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
cleanup();
|
|
72
|
+
ok ? resolve(value as T) : reject(value);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const onAbort = () => settle(false, abortReason(signal!));
|
|
76
|
+
|
|
77
|
+
// timeout
|
|
78
|
+
timeoutId = setTimeout(() => {
|
|
79
|
+
settle(false, new Error(`Request timed out after ${timeoutMs}ms`));
|
|
80
|
+
}, timeoutMs);
|
|
81
|
+
|
|
82
|
+
// abort (optional)
|
|
83
|
+
if (signal) {
|
|
84
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
85
|
+
if (signal.aborted) return onAbort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// run
|
|
89
|
+
fn().then(
|
|
90
|
+
v => settle(true, v),
|
|
91
|
+
err => settle(false, err),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
}
|