af07faec11
- Raised the Bun minimum version to >=1.3.14 across package metadata, install scripts, and changelog notes. - Removed the Photon native image pipeline and added SIXEL-based `sixel` support in pi-natives. - Migrated coding-agent image handling and resizing to `Bun.Image`, including updated tests and a JPEG quality bump to 80. - Added HTTP/2 fetch bootstrap with HTTPS-only fallback and updated Bun build flags for autoload suppression/`--keep-names`.
312 lines
10 KiB
JavaScript
312 lines
10 KiB
JavaScript
import * as childProcess from "node:child_process";
|
|
import * as fs from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import packageJson from "../package.json" with { type: "json" };
|
|
import { embeddedAddon } from "./embedded-addon.js";
|
|
import { detectCompiledBinary, getAddonFilenames, resolveLoaderCandidates } from "./loader-state.js";
|
|
|
|
/**
|
|
* Native addon loader and bindings.
|
|
*
|
|
* Types are auto-generated by napi-rs in `native/index.d.ts`.
|
|
*/
|
|
|
|
function getNativesDir() {
|
|
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
if (xdgDataHome && fs.existsSync(path.join(xdgDataHome, "omp"))) {
|
|
return path.join(xdgDataHome, "omp", "natives");
|
|
}
|
|
return path.join(os.homedir(), ".omp", "natives");
|
|
}
|
|
|
|
const require_ = createRequire(import.meta.url);
|
|
const platformTag = `${process.platform}-${process.arch}`;
|
|
const packageVersion = packageJson.version;
|
|
const nativeDir = path.join(import.meta.dir, "..", "native");
|
|
const execDir = path.dirname(process.execPath);
|
|
const versionedDir = path.join(getNativesDir(), packageVersion);
|
|
const userDataDir =
|
|
process.platform === "win32"
|
|
? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "omp")
|
|
: path.join(os.homedir(), ".local", "bin");
|
|
|
|
const isCompiledBinary = detectCompiledBinary({
|
|
embeddedAddon,
|
|
env: process.env,
|
|
importMetaUrl: import.meta.url,
|
|
});
|
|
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
|
|
function getVariantOverride() {
|
|
const value = process.env.PI_NATIVE_VARIANT;
|
|
if (!value) return null;
|
|
if (value === "modern" || value === "baseline") return value;
|
|
return null;
|
|
}
|
|
|
|
function detectAvx2Support() {
|
|
if (process.arch !== "x64") {
|
|
return false;
|
|
}
|
|
|
|
if (process.platform === "linux") {
|
|
try {
|
|
const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
|
|
return /\bavx2\b/i.test(cpuInfo);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (process.platform === "darwin") {
|
|
const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
|
|
if (leaf7 && /\bAVX2\b/i.test(leaf7)) {
|
|
return true;
|
|
}
|
|
const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
|
|
return Boolean(features && /\bAVX2\b/i.test(features));
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
const output = runCommand("powershell.exe", [
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-Command",
|
|
"[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
|
|
]);
|
|
return output && output.toLowerCase() === "true";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function resolveCpuVariant(override) {
|
|
if (process.arch !== "x64") return null;
|
|
if (override) return override;
|
|
return detectAvx2Support() ? "modern" : "baseline";
|
|
}
|
|
|
|
const variantOverride = getVariantOverride();
|
|
const selectedVariant = resolveCpuVariant(variantOverride);
|
|
const addonFilenames = getAddonFilenames({ tag: platformTag, arch: process.arch, variant: selectedVariant });
|
|
const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
|
|
|
|
const dedupedCandidates = resolveLoaderCandidates({
|
|
addonFilenames,
|
|
isCompiledBinary,
|
|
nativeDir,
|
|
execDir,
|
|
versionedDir,
|
|
userDataDir,
|
|
});
|
|
|
|
function runCommand(command, args) {
|
|
try {
|
|
const result = childProcess.spawnSync(command, args, { encoding: "utf-8" });
|
|
if (result.error) return null;
|
|
if (result.status !== 0) return null;
|
|
return (result.stdout || "").trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function selectEmbeddedAddonFile() {
|
|
if (!embeddedAddon) return null;
|
|
const defaultFile = embeddedAddon.files.find(file => file.variant === "default") || null;
|
|
if (process.arch !== "x64") return defaultFile || embeddedAddon.files[0] || null;
|
|
if (selectedVariant === "modern") {
|
|
return (
|
|
embeddedAddon.files.find(file => file.variant === "modern") ||
|
|
embeddedAddon.files.find(file => file.variant === "baseline") ||
|
|
null
|
|
);
|
|
}
|
|
return embeddedAddon.files.find(file => file.variant === "baseline") || null;
|
|
}
|
|
|
|
function maybeExtractEmbeddedAddon(errors) {
|
|
if (!isCompiledBinary || !embeddedAddon) return null;
|
|
if (embeddedAddon.platformTag !== platformTag || embeddedAddon.version !== packageVersion) return null;
|
|
|
|
const selectedEmbeddedFile = selectEmbeddedAddonFile();
|
|
if (!selectedEmbeddedFile) return null;
|
|
const targetPath = path.join(versionedDir, selectedEmbeddedFile.filename);
|
|
|
|
try {
|
|
fs.mkdirSync(versionedDir, { recursive: true });
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
errors.push(`embedded addon dir: ${message}`);
|
|
return null;
|
|
}
|
|
|
|
if (fs.existsSync(targetPath)) {
|
|
return targetPath;
|
|
}
|
|
|
|
try {
|
|
const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
|
|
fs.writeFileSync(targetPath, buffer);
|
|
return targetPath;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function loadNative() {
|
|
const errors = [];
|
|
const embeddedCandidate = maybeExtractEmbeddedAddon(errors);
|
|
const runtimeCandidates = embeddedCandidate ? [embeddedCandidate, ...dedupedCandidates] : dedupedCandidates;
|
|
for (const candidate of runtimeCandidates) {
|
|
try {
|
|
const bindings = require_(candidate);
|
|
return bindings;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
errors.push(`${candidate}: ${message}`);
|
|
}
|
|
}
|
|
// Check if this is an unsupported platform
|
|
if (!SUPPORTED_PLATFORMS.includes(platformTag)) {
|
|
throw new Error(
|
|
`Unsupported platform: ${platformTag}\n` +
|
|
`Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
|
|
"If you need support for this platform, please open an issue.",
|
|
);
|
|
}
|
|
const details = errors.map(error => `- ${error}`).join("\n");
|
|
let helpMessage;
|
|
if (isCompiledBinary) {
|
|
const expectedPaths = addonFilenames.map(filename => ` ${path.join(versionedDir, filename)}`).join("\n");
|
|
const downloadHints = addonFilenames
|
|
.map(filename => {
|
|
const downloadUrl = `https://github.com/can1357/oh-my-pi/releases/latest/download/${filename}`;
|
|
const targetPath = path.join(versionedDir, filename);
|
|
return ` curl -fsSL "${downloadUrl}" -o "${targetPath}"`;
|
|
})
|
|
.join("\n");
|
|
helpMessage =
|
|
`The compiled binary should extract one of:\n${expectedPaths}\n\n` +
|
|
`If missing, delete ${versionedDir} and re-run, or download manually:\n${downloadHints}`;
|
|
} else {
|
|
helpMessage =
|
|
"If installed via npm/bun, try reinstalling: bun install @oh-my-pi/pi-natives\n" +
|
|
"If developing locally, build with: bun --cwd=packages/natives run build\n" +
|
|
"Optional x64 variants: TARGET_VARIANT=baseline|modern bun --cwd=packages/natives run build";
|
|
}
|
|
|
|
throw new Error(`Failed to load pi_natives native addon for ${addonLabel}.\n\nTried:\n${details}\n\n${helpMessage}`);
|
|
}
|
|
|
|
const nativeBindings = loadNative();
|
|
// --- generated native exports (do not edit) ---
|
|
// classes
|
|
export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver;
|
|
export const MacOSPowerAssertion = nativeBindings.MacOSPowerAssertion;
|
|
export const Process = nativeBindings.Process;
|
|
export const PtySession = nativeBindings.PtySession;
|
|
export const Shell = nativeBindings.Shell;
|
|
|
|
// functions
|
|
export const astEdit = nativeBindings.astEdit;
|
|
export const astGrep = nativeBindings.astGrep;
|
|
export const copyToClipboard = nativeBindings.copyToClipboard;
|
|
export const countTokens = nativeBindings.countTokens;
|
|
export const detectMacOSAppearance = nativeBindings.detectMacOSAppearance;
|
|
export const encodeSixel = nativeBindings.encodeSixel;
|
|
export const executeShell = nativeBindings.executeShell;
|
|
export const extractSegments = nativeBindings.extractSegments;
|
|
export const fuzzyFind = nativeBindings.fuzzyFind;
|
|
export const getSupportedLanguages = nativeBindings.getSupportedLanguages;
|
|
export const getWorkProfile = nativeBindings.getWorkProfile;
|
|
export const glob = nativeBindings.glob;
|
|
export const grep = nativeBindings.grep;
|
|
export const hasMatch = nativeBindings.hasMatch;
|
|
export const highlightCode = nativeBindings.highlightCode;
|
|
export const htmlToMarkdown = nativeBindings.htmlToMarkdown;
|
|
export const invalidateFsScanCache = nativeBindings.invalidateFsScanCache;
|
|
export const isoBackend = nativeBindings.isoBackend;
|
|
export const isoDiff = nativeBindings.isoDiff;
|
|
export const isoIsUnavailableError = nativeBindings.isoIsUnavailableError;
|
|
export const isoProbe = nativeBindings.isoProbe;
|
|
export const isoResolve = nativeBindings.isoResolve;
|
|
export const isoStart = nativeBindings.isoStart;
|
|
export const isoStop = nativeBindings.isoStop;
|
|
export const listWorkspace = nativeBindings.listWorkspace;
|
|
export const matchesKey = nativeBindings.matchesKey;
|
|
export const matchesKittySequence = nativeBindings.matchesKittySequence;
|
|
export const matchesLegacySequence = nativeBindings.matchesLegacySequence;
|
|
export const parseKey = nativeBindings.parseKey;
|
|
export const parseKittySequence = nativeBindings.parseKittySequence;
|
|
export const readImageFromClipboard = nativeBindings.readImageFromClipboard;
|
|
export const sanitizeText = nativeBindings.sanitizeText;
|
|
export const search = nativeBindings.search;
|
|
export const sliceWithWidth = nativeBindings.sliceWithWidth;
|
|
export const summarizeCode = nativeBindings.summarizeCode;
|
|
export const supportsLanguage = nativeBindings.supportsLanguage;
|
|
export const truncateToWidth = nativeBindings.truncateToWidth;
|
|
export const visibleWidth = nativeBindings.visibleWidth;
|
|
export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi;
|
|
|
|
// string/numeric enums (napi-rs string_enum produces TS-only const enum)
|
|
export const AstMatchStrictness = {
|
|
Cst: "cst",
|
|
Smart: "smart",
|
|
Ast: "ast",
|
|
Relaxed: "relaxed",
|
|
Signature: "signature",
|
|
Template: "template",
|
|
};
|
|
export const Ellipsis = {
|
|
Unicode: 0,
|
|
Ascii: 1,
|
|
Omit: 2,
|
|
};
|
|
export const Encoding = {
|
|
O200kBase: "O200kBase",
|
|
Cl100kBase: "Cl100kBase",
|
|
};
|
|
export const FileType = {
|
|
File: 1,
|
|
Dir: 2,
|
|
Symlink: 3,
|
|
};
|
|
export const GrepOutputMode = {
|
|
Content: "content",
|
|
Count: "count",
|
|
FilesWithMatches: "filesWithMatches",
|
|
};
|
|
export const IsoBackendKind = {
|
|
Apfs: 0,
|
|
Btrfs: 1,
|
|
Zfs: 2,
|
|
LinuxReflink: 3,
|
|
Overlayfs: 4,
|
|
WindowsBlockClone: 5,
|
|
Projfs: 6,
|
|
Rcopy: 7,
|
|
};
|
|
export const IsoChangeKind = {
|
|
Added: 0,
|
|
Modified: 1,
|
|
Removed: 2,
|
|
};
|
|
export const KeyEventType = {
|
|
Press: 1,
|
|
Repeat: 2,
|
|
Release: 3,
|
|
};
|
|
export const MacOSAppearance = {
|
|
Dark: "dark",
|
|
Light: "light",
|
|
};
|
|
export const ProcessStatus = {
|
|
Running: "running",
|
|
Exited: "exited",
|
|
};
|
|
// --- end generated native exports ---
|