@oh-my-pi/pi-natives 12.7.6 → 12.8.0

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 CHANGED
@@ -52,8 +52,9 @@ crates/pi-natives/ # Rust source (workspace member)
52
52
  src/image.rs # Image processing (photon-rs)
53
53
  Cargo.toml # Rust dependencies
54
54
  native/ # Native addon binaries
55
- pi_natives.<platform>-<arch>.node
56
- pi_natives.node
55
+ pi_natives.<platform>-<arch>-modern.node # x64 modern ISA (AVX2)
56
+ pi_natives.<platform>-<arch>-baseline.node # x64 baseline ISA
57
+ pi_natives.<platform>-<arch>.node # non-x64 build artifact
57
58
  src/ # TypeScript wrappers
58
59
  native.ts # Native addon loader
59
60
  index.ts # Public API
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-natives",
3
- "version": "12.7.6",
3
+ "version": "12.8.0",
4
4
  "description": "Native Rust functionality via N-API",
5
5
  "keywords": ["napi", "rust", "native", "grep", "text-processing"],
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "url": "https://github.com/can1357/oh-my-pi/issues"
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-utils": "12.7.6"
40
+ "@oh-my-pi/pi-utils": "12.8.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/bun": "^1.3.9"
@@ -1,7 +1,15 @@
1
+ export type EmbeddedAddonVariant = "modern" | "baseline" | "default";
2
+
3
+ export interface EmbeddedAddonFile {
4
+ variant: EmbeddedAddonVariant;
5
+ filename: string;
6
+ filePath: string;
7
+ }
8
+
1
9
  export interface EmbeddedAddon {
2
- platform: string;
10
+ platformTag: string;
3
11
  version: string;
4
- filePath: string;
12
+ files: EmbeddedAddonFile[];
5
13
  }
6
14
 
7
15
  export const embeddedAddon: EmbeddedAddon | null = null;
package/src/native.ts CHANGED
@@ -3,19 +3,15 @@
3
3
  *
4
4
  * Each module extends NativeBindings via declaration merging in its types.ts.
5
5
  */
6
-
7
6
  import * as fs from "node:fs";
8
7
  import { createRequire } from "node:module";
9
8
  import * as os from "node:os";
10
9
  import * as path from "node:path";
11
10
  import { $env } from "@oh-my-pi/pi-utils";
12
11
  import { getNativesDir } from "@oh-my-pi/pi-utils/dirs";
13
-
14
12
  import packageJson from "../package.json";
15
13
  import type { NativeBindings } from "./bindings";
16
14
  import { embeddedAddon } from "./embedded-addon";
17
-
18
- // Import types to trigger declaration merging
19
15
  import "./clipboard/types";
20
16
  import "./glob/types";
21
17
  import "./grep/types";
@@ -32,50 +28,137 @@ import "./work/types";
32
28
 
33
29
  export type { NativeBindings, TsFunc } from "./bindings";
34
30
 
31
+ type CpuVariant = "modern" | "baseline";
35
32
  const require = createRequire(import.meta.url);
33
+ const textDecoder = new TextDecoder();
36
34
  const platformTag = `${process.platform}-${process.arch}`;
37
- const addonFilename = `pi_natives.${platformTag}.node`;
38
35
  const packageVersion = (packageJson as { version: string }).version;
39
36
  const nativeDir = path.join(import.meta.dir, "..", "native");
40
37
  const execDir = path.dirname(process.execPath);
41
38
  const versionedDir = path.join(getNativesDir(), packageVersion);
42
- const versionedAddonPath = path.join(versionedDir, addonFilename);
43
- const legacyUserDataDir =
39
+ const userDataDir =
44
40
  process.platform === "win32"
45
41
  ? path.join(Bun.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "omp")
46
42
  : path.join(os.homedir(), ".local", "bin");
47
- const downloadUrl = `https://github.com/can1357/oh-my-pi/releases/latest/download/${addonFilename}`;
48
43
  const isCompiledBinary =
49
44
  Bun.env.PI_COMPILED ||
50
45
  import.meta.url.includes("$bunfs") ||
51
46
  import.meta.url.includes("~BUN") ||
52
47
  import.meta.url.includes("%7EBUN");
53
-
54
48
  const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
55
49
 
56
- const debugCandidates = [path.join(nativeDir, "pi_natives.dev.node"), path.join(execDir, "pi_natives.dev.node")];
57
-
58
- const baseReleaseCandidates = [
59
- // Platform-tagged builds (preferred - always correct platform)
60
- path.join(nativeDir, addonFilename),
61
- path.join(execDir, addonFilename),
62
- // Fallback untagged (only created for native builds, not cross-compilation)
63
- path.join(nativeDir, "pi_natives.node"),
64
- path.join(execDir, "pi_natives.node"),
65
- ];
66
-
67
- const compiledCandidates = [
68
- versionedAddonPath,
69
- path.join(legacyUserDataDir, addonFilename),
70
- path.join(legacyUserDataDir, "pi_natives.node"),
71
- ];
50
+ const variantOverride = getVariantOverride();
51
+ const selectedVariant = resolveCpuVariant(variantOverride);
52
+ const addonFilenames = getAddonFilenames(platformTag, selectedVariant);
53
+ const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
72
54
 
55
+ const debugCandidates = [path.join(nativeDir, "pi_natives.dev.node"), path.join(execDir, "pi_natives.dev.node")];
56
+ const baseReleaseCandidates = addonFilenames.flatMap(filename => [
57
+ path.join(nativeDir, filename),
58
+ path.join(execDir, filename),
59
+ ]);
60
+ const compiledCandidates = addonFilenames.flatMap(filename => [
61
+ path.join(versionedDir, filename),
62
+ path.join(userDataDir, filename),
63
+ ]);
73
64
  const releaseCandidates = isCompiledBinary ? [...compiledCandidates, ...baseReleaseCandidates] : baseReleaseCandidates;
74
65
  const candidates = $env.PI_DEV ? [...debugCandidates, ...releaseCandidates] : releaseCandidates;
66
+ const dedupedCandidates = [...new Set(candidates)];
67
+
68
+ function decodeOutput(output: string | ArrayBufferView | ArrayBuffer | null | undefined): string {
69
+ if (!output) return "";
70
+ if (typeof output === "string") return output;
71
+ if (ArrayBuffer.isView(output))
72
+ return textDecoder.decode(new Uint8Array(output.buffer, output.byteOffset, output.byteLength));
73
+ return textDecoder.decode(new Uint8Array(output));
74
+ }
75
+
76
+ function runCommand(command: string, args: string[]): string | null {
77
+ try {
78
+ const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" });
79
+ if (result.exitCode !== 0) return null;
80
+ return decodeOutput(result.stdout).trim();
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function getVariantOverride(): CpuVariant | null {
87
+ const value = Bun.env.PI_NATIVE_VARIANT;
88
+ if (!value) return null;
89
+ if (value === "modern" || value === "baseline") return value;
90
+ return null;
91
+ }
92
+
93
+ function detectAvx2Support(): boolean {
94
+ if (process.arch !== "x64") return false;
95
+
96
+ if (process.platform === "linux") {
97
+ try {
98
+ const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
99
+ return /\bavx2\b/i.test(cpuInfo);
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ if (process.platform === "darwin") {
106
+ const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
107
+ if (leaf7 && /\bAVX2\b/i.test(leaf7)) return true;
108
+ const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
109
+ return Boolean(features && /\bAVX2\b/i.test(features));
110
+ }
75
111
 
112
+ if (process.platform === "win32") {
113
+ const output = runCommand("powershell.exe", [
114
+ "-NoProfile",
115
+ "-NonInteractive",
116
+ "-Command",
117
+ "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
118
+ ]);
119
+ return output?.toLowerCase() === "true";
120
+ }
121
+
122
+ return false;
123
+ }
124
+
125
+ function resolveCpuVariant(override: CpuVariant | null): CpuVariant | null {
126
+ if (process.arch !== "x64") return null;
127
+ if (override) return override;
128
+ return detectAvx2Support() ? "modern" : "baseline";
129
+ }
130
+
131
+ function getAddonFilenames(tag: string, variant: CpuVariant | null): string[] {
132
+ const defaultFilename = `pi_natives.${tag}.node`;
133
+ if (process.arch !== "x64" || !variant) return [defaultFilename];
134
+ const baselineFilename = `pi_natives.${tag}-baseline.node`;
135
+ const modernFilename = `pi_natives.${tag}-modern.node`;
136
+ if (variant === "modern") {
137
+ return [modernFilename, baselineFilename];
138
+ }
139
+ return [baselineFilename];
140
+ }
141
+
142
+ function selectEmbeddedAddonFile(): { filename: string; filePath: string } | null {
143
+ if (!embeddedAddon) return null;
144
+ const defaultFile = embeddedAddon.files.find(file => file.variant === "default") ?? null;
145
+ if (process.arch !== "x64") return defaultFile ?? embeddedAddon.files[0] ?? null;
146
+ if (selectedVariant === "modern") {
147
+ return (
148
+ embeddedAddon.files.find(file => file.variant === "modern") ??
149
+ embeddedAddon.files.find(file => file.variant === "baseline") ??
150
+ null
151
+ );
152
+ }
153
+ return embeddedAddon.files.find(file => file.variant === "baseline") ?? null;
154
+ }
76
155
  function maybeExtractEmbeddedAddon(errors: string[]): string | null {
77
156
  if (!isCompiledBinary || !embeddedAddon) return null;
78
- if (embeddedAddon.platform !== platformTag || embeddedAddon.version !== packageVersion) return null;
157
+ if (embeddedAddon.platformTag !== platformTag || embeddedAddon.version !== packageVersion) return null;
158
+
159
+ const selectedEmbeddedFile = selectEmbeddedAddonFile();
160
+ if (!selectedEmbeddedFile) return null;
161
+ const targetPath = path.join(versionedDir, selectedEmbeddedFile.filename);
79
162
 
80
163
  try {
81
164
  fs.mkdirSync(versionedDir, { recursive: true });
@@ -85,26 +168,24 @@ function maybeExtractEmbeddedAddon(errors: string[]): string | null {
85
168
  return null;
86
169
  }
87
170
 
88
- if (fs.existsSync(versionedAddonPath)) {
89
- return versionedAddonPath;
171
+ if (fs.existsSync(targetPath)) {
172
+ return targetPath;
90
173
  }
91
174
 
92
175
  try {
93
- const buffer = fs.readFileSync(embeddedAddon.filePath);
94
- fs.writeFileSync(versionedAddonPath, buffer);
95
- return versionedAddonPath;
176
+ const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
177
+ fs.writeFileSync(targetPath, buffer);
178
+ return targetPath;
96
179
  } catch (err) {
97
180
  const message = err instanceof Error ? err.message : String(err);
98
- errors.push(`embedded addon write: ${message}`);
181
+ errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
99
182
  return null;
100
183
  }
101
184
  }
102
-
103
185
  function loadNative(): NativeBindings {
104
186
  const errors: string[] = [];
105
187
  const embeddedCandidate = maybeExtractEmbeddedAddon(errors);
106
- const runtimeCandidates = embeddedCandidate ? [embeddedCandidate, ...candidates] : candidates;
107
-
188
+ const runtimeCandidates = embeddedCandidate ? [embeddedCandidate, ...dedupedCandidates] : dedupedCandidates;
108
189
  for (const candidate of runtimeCandidates) {
109
190
  try {
110
191
  const bindings = require(candidate) as NativeBindings;
@@ -121,7 +202,6 @@ function loadNative(): NativeBindings {
121
202
  errors.push(`${candidate}: ${message}`);
122
203
  }
123
204
  }
124
-
125
205
  // Check if this is an unsupported platform
126
206
  if (!SUPPORTED_PLATFORMS.includes(platformTag)) {
127
207
  throw new Error(
@@ -130,26 +210,29 @@ function loadNative(): NativeBindings {
130
210
  "If you need support for this platform, please open an issue.",
131
211
  );
132
212
  }
133
-
134
213
  const details = errors.map(error => `- ${error}`).join("\n");
135
214
  let helpMessage: string;
136
215
  if (isCompiledBinary) {
216
+ const expectedPaths = addonFilenames.map(filename => ` ${path.join(versionedDir, filename)}`).join("\n");
217
+ const downloadHints = addonFilenames
218
+ .map(filename => {
219
+ const downloadUrl = `https://github.com/can1357/oh-my-pi/releases/latest/download/${filename}`;
220
+ const targetPath = path.join(versionedDir, filename);
221
+ return ` curl -fsSL "${downloadUrl}" -o "${targetPath}"`;
222
+ })
223
+ .join("\n");
137
224
  helpMessage =
138
- `The compiled binary should extract the native addon to:\n` +
139
- ` ${versionedAddonPath}\n\n` +
140
- `If it is missing, delete ${versionedDir} and re-run, or download manually:\n` +
141
- ` curl -fsSL "${downloadUrl}" -o "${versionedAddonPath}"`;
225
+ `The compiled binary should extract one of:\n${expectedPaths}\n\n` +
226
+ `If missing, delete ${versionedDir} and re-run, or download manually:\n${downloadHints}`;
142
227
  } else {
143
228
  helpMessage =
144
229
  "If installed via npm/bun, try reinstalling: bun install @oh-my-pi/pi-natives\n" +
145
- "If developing locally, build with: bun --cwd=packages/natives run build:native";
230
+ "If developing locally, build with: bun --cwd=packages/natives run build:native\n" +
231
+ "Optional x64 variants: TARGET_VARIANT=baseline|modern bun --cwd=packages/natives run build:native";
146
232
  }
147
233
 
148
- throw new Error(
149
- `Failed to load pi_natives native addon for ${platformTag}.\n\n` + `Tried:\n${details}\n\n${helpMessage}`,
150
- );
234
+ throw new Error(`Failed to load pi_natives native addon for ${addonLabel}.\n\nTried:\n${details}\n\n${helpMessage}`);
151
235
  }
152
-
153
236
  function validateNative(bindings: NativeBindings, source: string): void {
154
237
  const missing: string[] = [];
155
238
  const checkFn = (name: keyof NativeBindings) => {
@@ -157,7 +240,6 @@ function validateNative(bindings: NativeBindings, source: string): void {
157
240
  missing.push(name);
158
241
  }
159
242
  };
160
-
161
243
  checkFn("copyToClipboard");
162
244
  checkFn("readImageFromClipboard");
163
245
  checkFn("glob");
@@ -171,7 +253,6 @@ function validateNative(bindings: NativeBindings, source: string): void {
171
253
  checkFn("getSupportedLanguages");
172
254
  checkFn("truncateToWidth");
173
255
  checkFn("sanitizeText");
174
-
175
256
  checkFn("wrapTextWithAnsi");
176
257
  checkFn("sliceWithWidth");
177
258
  checkFn("extractSegments");
@@ -189,7 +270,6 @@ function validateNative(bindings: NativeBindings, source: string): void {
189
270
  checkFn("getSystemInfo");
190
271
  checkFn("getWorkProfile");
191
272
  checkFn("invalidateFsScanCache");
192
-
193
273
  if (missing.length) {
194
274
  throw new Error(
195
275
  `Native addon missing exports (${source}). Missing: ${missing.join(", ")}. ` +
@@ -197,5 +277,4 @@ function validateNative(bindings: NativeBindings, source: string): void {
197
277
  );
198
278
  }
199
279
  }
200
-
201
280
  export const native = loadNative();