From f0a471c590862c03b75c8ed5fe530ccc7e3df82e Mon Sep 17 00:00:00 2001 From: Zireael <3856578+Zireael@users.noreply.github.com> Date: Tue, 26 May 2026 09:11:36 +0200 Subject: [PATCH 1/2] fix(windows): resolve Windows binary, ONNX, and storage bugs Closes #64. Windows-specific fixes across the TypeScript and Rust layers: - **Binary version cache** (resolver.ts): add `findHighestCachedVersion()` to fall back to a newer cached binary when the exact plugin version isn't cached. - **Windows rename safety** (resolver.ts, downloader.ts): `unlinkSync` before `renameSync` to avoid `EEXIST` on Windows. - **ONNX Runtime detection** (onnx-runtime.ts, onnx.ts): search `%ProgramFiles%`, NuGet cache, and `%PATH%` directories for `onnxruntime.dll`. - **Storage directory** (opencode.ts): return `%LOCALAPPDATA%\cortexkit\aft` on Windows instead of falling through to the Unix path. - **Doctor output** (doctor.ts): issues summary section for actionable bullets; green `log.success()` instead of red `outro()` for the "looks good" message. - **Rust pre-validation** (semantic_index.rs): `pre_validate_onnx_runtime` now actually validates on Windows via `LoadLibraryExW` instead of being a no-op; `ONNX_RUNTIME_INSTALL_HINT` includes Windows guidance. - **Storage dir creation** (configure.rs): `fs::create_dir_all` in `handle_configure` so the root exists immediately after configure. - **Diagnostic robustness** (diagnostics.ts): best-effort `mkdirSync` in `diagnoseHarness` before reading storage subtrees. All Rust changes verified via `cargo check --workspace --all-targets` and `cargo clippy --workspace --all-targets --all-features -- -D warnings`. --- crates/aft/src/commands/configure.rs | 15 +++ crates/aft/src/semantic_index.rs | 42 +++++++- packages/aft-bridge/src/downloader.ts | 9 +- packages/aft-bridge/src/onnx-runtime.ts | 89 +++++++++++++--- packages/aft-bridge/src/resolver.ts | 120 ++++++++++++++++++++-- packages/aft-cli/src/adapters/opencode.ts | 13 ++- packages/aft-cli/src/commands/doctor.ts | 46 ++++++++- packages/aft-cli/src/lib/diagnostics.ts | 14 ++- packages/aft-cli/src/lib/onnx.ts | 48 +++++++-- 9 files changed, 358 insertions(+), 38 deletions(-) diff --git a/crates/aft/src/commands/configure.rs b/crates/aft/src/commands/configure.rs index 368d13d1..cae32139 100644 --- a/crates/aft/src/commands/configure.rs +++ b/crates/aft/src/commands/configure.rs @@ -1,3 +1,4 @@ +use std::fs; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::path::{Component, Path, PathBuf}; use std::process::Command; @@ -1207,6 +1208,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { Ok(path) => Some(path), Err(error) => return Response::error(&req.id, "invalid_request", error), }; + } if let Some(raw) = params.get("url_fetch_allow_private") { let Some(value) = raw.as_bool() else { @@ -1356,6 +1358,19 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { &canonical_cache_root, )); if let Some(storage_dir) = next_config.storage_dir.clone() { + // Ensure the storage root directory exists so subsystems (trust, + // backups, checkpoints, DB, persistence) can create their sub-trees + // without a separate create_dir_all per subsystem. On fresh installs + // this directory hasn't been created yet, and every subsystem + // currently creates its own subdirectory lazily — but the root must + // exist for status/diagnostics to report a valid path. + if let Err(err) = fs::create_dir_all(&storage_dir) { + slog_warn!( + "failed to create storage directory {}: {}", + storage_dir.display(), + err + ); + } ctx.backup().borrow_mut().set_storage_dir_for_harness( storage_dir, harness, diff --git a/crates/aft/src/semantic_index.rs b/crates/aft/src/semantic_index.rs index 511cd88e..33300a42 100644 --- a/crates/aft/src/semantic_index.rs +++ b/crates/aft/src/semantic_index.rs @@ -30,7 +30,9 @@ const F32_BYTES: usize = std::mem::size_of::(); const HEADER_BYTES_V1: usize = 9; const HEADER_BYTES_V2: usize = 13; const ONNX_RUNTIME_INSTALL_HINT: &str = - "ONNX Runtime not found. Install via: brew install onnxruntime (macOS) or apt install libonnxruntime (Linux)."; + "ONNX Runtime not found. Install via: brew install onnxruntime (macOS), \ + apt install libonnxruntime (Linux), or place onnxruntime.dll in your PATH (Windows). \ + AFT can auto-download ONNX Runtime — run `npx @cortexkit/aft doctor` to diagnose."; const SEMANTIC_INDEX_VERSION_V1: u8 = 1; const SEMANTIC_INDEX_VERSION_V2: u8 = 2; @@ -788,8 +790,42 @@ pub fn pre_validate_onnx_runtime() -> Result<(), String> { #[cfg(target_os = "windows")] { - // On Windows, skip pre-validation — let ort handle LoadLibrary - let _ = dylib_path; + // Validate ONNX Runtime availability on Windows by loading the DLL + // via LoadLibraryExW before the ort crate attempts its own LoadLibrary. + // This way we can produce a friendly error (with installation hints) + // instead of a raw LoadLibrary failure from deep inside fastembed. + let lib_name = dylib_path.as_deref().unwrap_or("onnxruntime.dll"); + + // Use kernel32 LoadLibraryExW for the validation — built-in, no + // crate dependency required. + #[link(name = "kernel32")] + extern "system" { + fn LoadLibraryExW( + lpLibFileName: *const u16, + hFile: *mut std::ffi::c_void, + dwFlags: u32, + ) -> *mut std::ffi::c_void; + fn FreeLibrary(hLibModule: *mut std::ffi::c_void) -> i32; + } + + unsafe { + use std::os::windows::ffi::OsStrExt; + let wide: Vec = std::ffi::OsStr::new(lib_name) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let handle = LoadLibraryExW(wide.as_ptr(), std::ptr::null_mut(), 0); + if handle.is_null() { + let err = std::io::Error::last_os_error(); + return Err(format!( + "ONNX Runtime not found. LoadLibraryExW('{}') failed: {}. \ + Run `npx @cortexkit/aft doctor` to diagnose.", + lib_name, err + )); + } + FreeLibrary(handle); + } } Ok(()) diff --git a/packages/aft-bridge/src/downloader.ts b/packages/aft-bridge/src/downloader.ts index 6f7d2651..84e83dcb 100644 --- a/packages/aft-bridge/src/downloader.ts +++ b/packages/aft-bridge/src/downloader.ts @@ -269,7 +269,14 @@ export async function downloadBinary(version?: string): Promise { chmodSync(tmpPath, 0o755); } - // Atomic rename + // Atomic rename — unlink first on Windows where renameSync fails if target exists + if (process.platform === "win32" && existsSync(binaryPath)) { + try { + unlinkSync(binaryPath); + } catch { + // best-effort; renameSync will surface the error if unlink fails + } + } renameSync(tmpPath, binaryPath); log(`AFT binary ready at ${binaryPath}`); diff --git a/packages/aft-bridge/src/onnx-runtime.ts b/packages/aft-bridge/src/onnx-runtime.ts index 6cf6ac73..64211d00 100644 --- a/packages/aft-bridge/src/onnx-runtime.ts +++ b/packages/aft-bridge/src/onnx-runtime.ts @@ -430,22 +430,87 @@ function findSystemOnnxRuntime(libName?: string): string | null { "/usr/lib/aarch64-linux-gnu", "/usr/local/lib", ); + } else if (process.platform === "win32") { + // Common Windows install locations for ONNX Runtime + const programFiles = process.env.ProgramFiles ?? "C:\\Program Files"; + const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; + searchPaths.push( + join(programFiles, "onnxruntime", "lib"), + join(programFiles, "Microsoft ONNX Runtime", "lib"), + join(programFiles, "Microsoft Machine Learning", "lib"), + join(programFilesX86, "onnxruntime", "lib"), + // Windows NuGet package layout: + // \.nuget\packages\microsoft.ml.onnxruntime\\runtimes\win-{x64,arm64}\native\ + // Scan all installed versions since we don't know which one is present. + ...(() => { + const nugetPaths: string[] = []; + const userProfile = process.env.USERPROFILE ?? ""; + if (!userProfile) return nugetPaths; + const nugetPackageDir = join(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime"); + if (!existsSync(nugetPackageDir)) return nugetPaths; + try { + for (const entry of readdirSync(nugetPackageDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + // Skip well-known non-version entries + if (entry.name === "__globalPackagesFolder" || entry.name.startsWith(".")) continue; + nugetPaths.push( + join(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), + join(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"), + ); + } + } catch { + // best-effort scan + } + return nugetPaths; + })(), + ); + // Also search PATH directories for onnxruntime.dll + const pathEnv = process.env.PATH ?? ""; + for (const dir of pathEnv.split(";")) { + const trimmed = dir.trim(); + if (!trimmed) continue; + searchPaths.push(trimmed); + } } - for (const dir of searchPaths) { + // Deduplicate paths while preserving order. + // On case-insensitive filesystems (Windows, macOS) normalize casing for + // comparison; on Linux the raw path casing is the authority. + const normalizeCase = process.platform === "win32" || process.platform === "darwin"; + const seen = new Set(); + const uniquePaths = searchPaths.filter((p) => { + let key = resolve(p).replace(/[/\\]+$/, ""); + if (normalizeCase) key = key.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + for (const dir of uniquePaths) { if (!existsSync(join(dir, libName))) continue; - // Reject system installs that the Rust pre-validator will refuse. Without - // this filter, a stale distro package (e.g. libonnxruntime1.9 on Ubuntu - // 22.04) shadows our auto-downloaded v1.24 forever and semantic search - // stays "failed" until the user hand-deletes the system library. - const version = detectOnnxVersion(dir, libName); - if (version && !isOnnxVersionCompatible(version)) { - warn( - `Skipping system ONNX Runtime at ${dir} (v${version}); AFT requires ` + - `v${REQUIRED_ORT_MAJOR}.${REQUIRED_ORT_MIN_MINOR}+. Falling through to AFT-managed download.`, - ); - continue; + // Skip the version check for PATH entries — version-suffixed filenames + // are less common on Windows and we want PATH discovery to succeed. + let skipVersionCheck = false; + if (process.platform === "win32") { + // Only do version check for common install paths, not PATH entries + const isCommonPath = dir.includes("Program Files") || dir.includes("onnxruntime"); + if (!isCommonPath) skipVersionCheck = true; + } + + if (!skipVersionCheck) { + // Reject system installs that the Rust pre-validator will refuse. Without + // this filter, a stale distro package (e.g. libonnxruntime1.9 on Ubuntu + // 22.04) shadows our auto-downloaded v1.24 forever and semantic search + // stays "failed" until the user hand-deletes the system library. + const version = detectOnnxVersion(dir, libName); + if (version && !isOnnxVersionCompatible(version)) { + warn( + `Skipping system ONNX Runtime at ${dir} (v${version}); AFT requires ` + + `v${REQUIRED_ORT_MAJOR}.${REQUIRED_ORT_MIN_MINOR}+. Falling through to AFT-managed download.`, + ); + continue; + } } return dir; diff --git a/packages/aft-bridge/src/resolver.ts b/packages/aft-bridge/src/resolver.ts index 2f03b036..d41c7685 100644 --- a/packages/aft-bridge/src/resolver.ts +++ b/packages/aft-bridge/src/resolver.ts @@ -1,5 +1,5 @@ -import { execSync } from "node:child_process"; -import { chmodSync, copyFileSync, existsSync, mkdirSync, renameSync } from "node:fs"; +import { execSync, spawnSync } from "node:child_process"; +import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, renameSync, unlinkSync } from "node:fs"; import { createRequire } from "node:module"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -55,6 +55,14 @@ function copyToVersionedCache(npmBinaryPath: string, knownVersion?: string): str if (process.platform !== "win32") { chmodSync(tmpPath, 0o755); } + // Atomic rename — unlink first on Windows where renameSync fails if target exists + if (process.platform === "win32" && existsSync(cachedPath)) { + try { + unlinkSync(cachedPath); + } catch { + // best-effort; renameSync will surface the error if unlink fails + } + } renameSync(tmpPath, cachedPath); log(`Copied npm binary to versioned cache: ${cachedPath}`); return cachedPath; @@ -68,6 +76,22 @@ function normalizeBareVersion(version: string): string { return version.startsWith("v") ? version.slice(1) : version; } +/** + * Compare two semver strings. Returns >0 if a > b, <0 if a < b, 0 if equal. + * Both versions must be bare (no leading "v"). + */ +function compareSemver(a: string, b: string): number { + const aParts = a.split(".").map(Number); + const bParts = b.split(".").map(Number); + for (let i = 0; i < 3; i++) { + const av = aParts[i] ?? 0; + const bv = bParts[i] ?? 0; + if (av > bv) return 1; + if (av < bv) return -1; + } + return 0; +} + function homeDirFromEnv(env: ResolverEnv): string { return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir(); } @@ -123,6 +147,60 @@ function parsePathLookupOutput(output: string): string[] { .filter(Boolean); } +/** + * Scan the versioned cache directory for the highest available binary version. + * + * Iterates over `/v*/aft[.exe]` entries, reads each binary's version, + * and returns the one with the highest semantic version. This is the fallback + * when the exact plugin version isn't cached but a newer version was downloaded + * by `aft doctor --fix` or a previous `ensureBinary` call. + * + * @returns `{ path, version }` for the highest-versioned valid binary, or null. + */ +function findHighestCachedVersion( + cacheDir: string, + ext: string, +): { path: string; version: string } | null { + try { + const entries = readdirSync(cacheDir, { withFileTypes: true }); + let best: { path: string; version: string } | null = null; + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const tag = entry.name; + if (!tag.startsWith("v")) continue; + + const binaryPath = join(cacheDir, tag, `aft${ext}`); + if (!existsSync(binaryPath)) continue; + + const binaryVersion = readBinaryVersion(binaryPath); + if (binaryVersion === null) continue; + + if (best === null) { + best = { path: binaryPath, version: binaryVersion }; + } else { + // Compare semver strings + const current = best.version.split(".").map(Number); + const candidate = binaryVersion.split(".").map(Number); + let isHigher = false; + for (let i = 0; i < 3; i++) { + const a = current[i] ?? 0; + const b = candidate[i] ?? 0; + if (b > a) { isHigher = true; break; } + if (b < a) break; + } + if (isHigher) { + best = { path: binaryPath, version: binaryVersion }; + } + } + } + + return best; + } catch { + return null; + } +} + /** * Map the current `process.platform` and `process.arch` to the npm platform * package suffix (e.g. `"darwin-arm64"`, `"linux-x64"`). @@ -157,6 +235,10 @@ export function platformKey( /** * Locate the `aft` binary synchronously by checking (in order): * 1. Cached binary from previous auto-download (~/.cache/aft/bin/) + * a. First, check for the **exact** requested version in cache. + * b. Then, scan ALL cached versions and return the highest one (handles + * the case where `aft doctor --fix` downloaded a newer version but the + * plugin still resolves by its own older version). * 2. npm platform package via `require.resolve(@cortexkit/aft-/bin/aft)` * 3. PATH lookup via `which aft` (or `where aft` on Windows) * 4. ~/.cargo/bin/aft (Rust cargo install location) @@ -171,9 +253,6 @@ export function findBinarySync(expectedVersion?: string): string | null { const ext = process.platform === "win32" ? ".exe" : ""; const env = { ...process.env }; - // 1. Check versioned cache for the requested version (or this package's own - // version as a fallback so direct callers without a host still benefit from - // the cache). const pluginVersion = expectedVersion ?? (() => { @@ -184,10 +263,35 @@ export function findBinarySync(expectedVersion?: string): string | null { return null; } })(); + + // 1. Check versioned cache — first exact version, then highest fallback. if (pluginVersion) { const tag = pluginVersion.startsWith("v") ? pluginVersion : `v${pluginVersion}`; - const versionCached = cachedBinaryPathFromEnv(tag, env, ext); - if (versionCached && isExpectedCachedBinary(versionCached, pluginVersion)) return versionCached; + + // 1a. Exact version in cache. + const exactCached = cachedBinaryPathFromEnv(tag, env, ext); + if (exactCached && isExpectedCachedBinary(exactCached, pluginVersion)) return exactCached; + + // 1b. Scan ALL cached versions for the highest available one. This handles + // the case where `aft doctor --fix` downloaded v0.30.3 but the plugin + // resolves by its own older version (v0.29.1). Preferring the newer binary + // is safe because the protocol is backwards compatible. + const cacheDir = cacheDirFromEnv(env); + const highestCached = findHighestCachedVersion(cacheDir, ext); + if (highestCached) { + const highestBare = normalizeBareVersion(highestCached.version); + // Only use the highest cached version if it's actually newer than the + // plugin version. A simple inequality check is not enough — the highest + // version in the cache could still be lower than the plugin's version + // (e.g. cache has v0.28.0 but plugin is v0.30.0). We never downgrade. + const pluginBare = normalizeBareVersion(pluginVersion); + if (compareSemver(highestBare, pluginBare) > 0) { + log( + `Using highest cached version ${highestCached.version} (plugin requested ${pluginVersion}) at ${highestCached.path}`, + ); + return highestCached.path; + } + } } // 2. Check npm platform package — copy to versioned cache to avoid @@ -257,7 +361,7 @@ export const __test__ = { * Locate the `aft` binary, with auto-download as a last resort. * * Resolution order: - * 1. Cached binary (~/.cache/aft/bin/) + * 1. Cached binary (~/.cache/aft/bin/) — exact version, then highest available * 2. npm platform package (@cortexkit/aft-) * 3. PATH lookup (which aft) * 4. ~/.cargo/bin/aft diff --git a/packages/aft-cli/src/adapters/opencode.ts b/packages/aft-cli/src/adapters/opencode.ts index 3bfc9271..fb45c110 100644 --- a/packages/aft-cli/src/adapters/opencode.ts +++ b/packages/aft-cli/src/adapters/opencode.ts @@ -219,8 +219,17 @@ export class OpenCodeAdapter implements HarnessAdapter { } getStorageDir(): string { - const xdg = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"); - return join(xdg, "opencode", "storage", "plugin", "aft"); + // Use the same CortexKit storage root as the AFT binary and migration code. + // The legacy path (~/.local/share/opencode/storage/plugin/aft on Unix) + // is only relevant for pre-v0.27 upgrades — after migration, data lives + // under the CortexKit root regardless of platform. + if (process.platform === "win32") { + const localAppData = + process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"); + return join(localAppData, "cortexkit", "aft"); + } + const xdgData = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"); + return join(xdgData, "cortexkit", "aft"); } getLogFile(): string { diff --git a/packages/aft-cli/src/commands/doctor.ts b/packages/aft-cli/src/commands/doctor.ts index b4d5e8cf..ebb9ea85 100644 --- a/packages/aft-cli/src/commands/doctor.ts +++ b/packages/aft-cli/src/commands/doctor.ts @@ -1,4 +1,4 @@ -import { existsSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { HarnessAdapter } from "../adapters/types.js"; @@ -170,6 +170,48 @@ export async function runDoctor(options: DoctorOptions): Promise { } if (hadProblems) { + // Build a focused issues summary so the user can distinguish diagnostics + // from problems that need action. Each bullet links the issue to a fix. + const issueBullets: string[] = []; + if (!report.binaryVersion) { + issueBullets.push( + "AFT binary not detected — run `aft doctor --fix` to download, or it will auto-install on first tool call.", + ); + } + for (const h of report.harnesses) { + if (!h.hostInstalled) { + continue; // Already warned inline + } + if (!h.pluginRegistered) { + issueBullets.push( + `${h.displayName}: plugin not registered — run \`aft setup\` or \`aft doctor --fix\` to fix.`, + ); + } + if (h.aftConfig.parseError) { + issueBullets.push( + `${h.displayName}: AFT config parse error — fix syntax in ${h.configPaths.aftConfig}.`, + ); + } + if (h.onnxRuntime.required) { + if (!h.onnxRuntime.cachedPath && !h.onnxRuntime.systemPath) { + issueBullets.push( + `${h.displayName}: ONNX Runtime not installed (required for semantic search) — ${h.onnxRuntime.installHint}`, + ); + } + if (h.onnxRuntime.cachedCompatible === false || h.onnxRuntime.systemCompatible === false) { + issueBullets.push( + `${h.displayName}: ONNX Runtime version incompatible — run \`aft doctor --fix\` to re-install.`, + ); + } + } + } + + if (issueBullets.length > 0) { + log.warn(`Issues found (${issueBullets.length}):`); + for (const bullet of issueBullets) { + log.error(` ${bullet}`); + } + } note( "Run `aft setup` or `aft doctor --fix` to register AFT with any harness showing `plugin registered: no`. Run `aft doctor --fix` for ONNX Runtime issues or to download a missing aft binary.", "Tips", @@ -177,7 +219,7 @@ export async function runDoctor(options: DoctorOptions): Promise { outro("Done — some issues found."); return 1; } - outro("Everything looks good."); + log.success("Everything looks good."); return 0; } diff --git a/packages/aft-cli/src/lib/diagnostics.ts b/packages/aft-cli/src/lib/diagnostics.ts index d322c100..45268431 100644 --- a/packages/aft-cli/src/lib/diagnostics.ts +++ b/packages/aft-cli/src/lib/diagnostics.ts @@ -1,4 +1,4 @@ -import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs"; +import { closeSync, existsSync, mkdirSync, openSync, readSync, statSync } from "node:fs"; import type { HarnessAdapter } from "../adapters/types.js"; import { type BinaryCacheInfo, getBinaryCacheInfo } from "./binary-cache.js"; import { probeBinaryVersion } from "./binary-probe.js"; @@ -95,6 +95,18 @@ async function diagnoseHarness(adapter: HarnessAdapter): Promise Record }) diff --git a/packages/aft-cli/src/lib/onnx.ts b/packages/aft-cli/src/lib/onnx.ts index 589e6e82..c460c1de 100644 --- a/packages/aft-cli/src/lib/onnx.ts +++ b/packages/aft-cli/src/lib/onnx.ts @@ -1,5 +1,5 @@ import { existsSync, readdirSync, readlinkSync, realpathSync, statSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; export const ONNX_RUNTIME_VERSION = "1.24.4"; @@ -31,15 +31,45 @@ export function getManualInstallHint(): string { export function findSystemOnnxRuntime(): string | null { const libName = getOnnxLibraryName(); - const searchPaths = - process.platform === "darwin" - ? ["/opt/homebrew/lib", "/usr/local/lib"] - : process.platform === "linux" - ? ["/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib"] - : []; + const searchPaths: string[] = []; - for (const path of searchPaths) { - if (existsSync(join(path, libName))) return path; + if (process.platform === "darwin") { + searchPaths.push("/opt/homebrew/lib", "/usr/local/lib"); + } else if (process.platform === "linux") { + searchPaths.push( + "/usr/lib", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", + "/usr/local/lib", + ); + } else if (process.platform === "win32") { + const programFiles = process.env.ProgramFiles ?? "C:\\Program Files"; + const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; + searchPaths.push( + join(programFiles, "onnxruntime", "lib"), + join(programFiles, "Microsoft ONNX Runtime", "lib"), + join(programFiles, "Microsoft Machine Learning", "lib"), + join(programFilesX86, "onnxruntime", "lib"), + ); + // Also search PATH for onnxruntime.dll + const pathEnv = process.env.PATH ?? ""; + for (const dir of pathEnv.split(";")) { + const trimmed = dir.trim(); + if (trimmed) searchPaths.push(trimmed); + } + } + + // Deduplicate paths. + // On case-insensitive filesystems (Windows, macOS) normalize casing for + // comparison; on Linux the raw path casing is the authority. + const normalizeCase = process.platform === "win32" || process.platform === "darwin"; + const seen = new Set(); + for (const dir of searchPaths) { + let key = resolve(dir).replace(/[/\\]+$/, ""); + if (normalizeCase) key = key.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + if (existsSync(join(dir, libName))) return dir; } return null; } From 70d677cf9034af3578ed73a014e86a1588ab23b7 Mon Sep 17 00:00:00 2001 From: Zireael <3856578+Zireael@users.noreply.github.com> Date: Tue, 26 May 2026 10:26:12 +0200 Subject: [PATCH 2/2] fix: escape */ in JSDoc to prevent TypeScript 6.0.3 early comment termination The JSDoc for findHighestCachedVersion contained the literal text 'v*/' which the TypeScript 6.0.3 tokenizer interprets as closing the multi-line comment, causing 62 cascading TS errors across the file. Replaced the glob pattern description with an explicit versioned-cache example that avoids the */ sequence. --- packages/aft-bridge/src/resolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aft-bridge/src/resolver.ts b/packages/aft-bridge/src/resolver.ts index d41c7685..ff37d6d1 100644 --- a/packages/aft-bridge/src/resolver.ts +++ b/packages/aft-bridge/src/resolver.ts @@ -150,12 +150,12 @@ function parsePathLookupOutput(output: string): string[] { /** * Scan the versioned cache directory for the highest available binary version. * - * Iterates over `/v*/aft[.exe]` entries, reads each binary's version, + * Iterates over versioned cache entries (e.g. `/v0.30.0/aft`), reads each binary's version, * and returns the one with the highest semantic version. This is the fallback * when the exact plugin version isn't cached but a newer version was downloaded * by `aft doctor --fix` or a previous `ensureBinary` call. * - * @returns `{ path, version }` for the highest-versioned valid binary, or null. + * @returns The path and version of the highest-versioned binary, or null. */ function findHighestCachedVersion( cacheDir: string,