@cortexkit/aft-pi 0.16.1 → 0.17.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/dist/index.js CHANGED
@@ -17,8 +17,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
17
17
 
18
18
  // src/index.ts
19
19
  import { createRequire as createRequire3 } from "node:module";
20
- import { homedir as homedir7 } from "node:os";
21
- import { join as join9 } from "node:path";
20
+ import { homedir as homedir8 } from "node:os";
21
+ import { join as join12 } from "node:path";
22
22
 
23
23
  // src/shared/status.ts
24
24
  function asRecord(value) {
@@ -13880,7 +13880,10 @@ var LspServerSchema = LspServerEntrySchema.extend({
13880
13880
  var LspConfigSchema = exports_external.object({
13881
13881
  servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
13882
13882
  disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
13883
- python: exports_external.enum(["pyright", "ty", "auto"]).optional()
13883
+ python: exports_external.enum(["pyright", "ty", "auto"]).optional(),
13884
+ auto_install: exports_external.boolean().optional(),
13885
+ grace_days: exports_external.number().int().positive().optional(),
13886
+ versions: exports_external.record(exports_external.string().trim().min(1), exports_external.string().trim().min(1)).optional()
13884
13887
  });
13885
13888
  var AftConfigSchema = exports_external.object({
13886
13889
  format_on_edit: exports_external.boolean().optional(),
@@ -14060,26 +14063,76 @@ function mergeSemanticConfig(base, override) {
14060
14063
  return;
14061
14064
  return Object.fromEntries(Object.entries(semantic).filter(([, v]) => v !== undefined));
14062
14065
  }
14066
+ function mergeLspConfig(base, override) {
14067
+ const projectSafe = {};
14068
+ if (override?.python !== undefined)
14069
+ projectSafe.python = override.python;
14070
+ const userDisabled = base?.disabled ?? [];
14071
+ const lsp = {
14072
+ ...base,
14073
+ ...projectSafe,
14074
+ ...userDisabled.length > 0 ? { disabled: [...userDisabled] } : {}
14075
+ };
14076
+ if (Object.values(lsp).every((v) => v === undefined))
14077
+ return;
14078
+ return Object.fromEntries(Object.entries(lsp).filter(([, v]) => v !== undefined));
14079
+ }
14080
+ function getProjectLspStrippedKeys(lsp) {
14081
+ if (!lsp)
14082
+ return [];
14083
+ const strippedKeys = [];
14084
+ if (lsp.servers !== undefined)
14085
+ strippedKeys.push("lsp.servers");
14086
+ if (lsp.versions !== undefined)
14087
+ strippedKeys.push("lsp.versions");
14088
+ if (lsp.auto_install !== undefined)
14089
+ strippedKeys.push("lsp.auto_install");
14090
+ if (lsp.grace_days !== undefined)
14091
+ strippedKeys.push("lsp.grace_days");
14092
+ if (lsp.disabled !== undefined)
14093
+ strippedKeys.push("lsp.disabled");
14094
+ return strippedKeys;
14095
+ }
14096
+ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
14097
+ "tool_surface",
14098
+ "format_on_edit",
14099
+ "validate_on_edit",
14100
+ "experimental_search_index",
14101
+ "experimental_semantic_search",
14102
+ "experimental_lsp_ty"
14103
+ ]);
14104
+ function pickProjectSafeFields(override) {
14105
+ const safe = {};
14106
+ for (const key of PROJECT_SAFE_TOP_LEVEL_FIELDS) {
14107
+ if (override[key] !== undefined) {
14108
+ safe[key] = override[key];
14109
+ }
14110
+ }
14111
+ return safe;
14112
+ }
14113
+ function getStrippedTopLevelKeys(override) {
14114
+ const stripped = [];
14115
+ if (override.restrict_to_project_root !== undefined)
14116
+ stripped.push("restrict_to_project_root");
14117
+ if (override.url_fetch_allow_private !== undefined)
14118
+ stripped.push("url_fetch_allow_private");
14119
+ if (override.max_callgraph_files !== undefined)
14120
+ stripped.push("max_callgraph_files");
14121
+ return stripped;
14122
+ }
14063
14123
  function mergeConfigs(base, override) {
14064
14124
  const disabledTools = [...base.disabled_tools ?? [], ...override.disabled_tools ?? []];
14065
14125
  const formatter = { ...base.formatter, ...override.formatter };
14066
14126
  const checker = { ...base.checker, ...override.checker };
14067
14127
  const semantic = mergeSemanticConfig(base.semantic, override.semantic);
14068
- const lspServers = { ...base.lsp?.servers, ...override.lsp?.servers };
14069
- const disabledLsp = [...base.lsp?.disabled ?? [], ...override.lsp?.disabled ?? []];
14070
- const lsp = {
14071
- ...base.lsp,
14072
- ...override.lsp,
14073
- ...Object.keys(lspServers).length > 0 ? { servers: lspServers } : {},
14074
- ...disabledLsp.length > 0 ? { disabled: [...new Set(disabledLsp)] } : {}
14075
- };
14076
- const { semantic: _stripSemantic, ...safeOverride } = override;
14128
+ const lsp = mergeLspConfig(base.lsp, override.lsp);
14129
+ const safeOverride = pickProjectSafeFields(override);
14077
14130
  return {
14078
14131
  ...base,
14079
14132
  ...safeOverride,
14080
14133
  ...Object.keys(formatter).length > 0 ? { formatter } : {},
14081
14134
  ...Object.keys(checker).length > 0 ? { checker } : {},
14082
- ...Object.values(lsp).some((value) => value !== undefined) ? { lsp } : {},
14135
+ ...lsp ? { lsp } : {},
14083
14136
  semantic,
14084
14137
  ...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
14085
14138
  };
@@ -14100,243 +14153,1791 @@ function loadAftConfig(projectDirectory) {
14100
14153
  if (projectConfig.semantic?.backend !== undefined || projectConfig.semantic?.base_url !== undefined || projectConfig.semantic?.api_key_env !== undefined) {
14101
14154
  warn("Ignoring semantic.backend/base_url/api_key_env from project config (security: use user config for external backends)");
14102
14155
  }
14156
+ const strippedLspKeys = getProjectLspStrippedKeys(projectConfig.lsp);
14157
+ if (strippedLspKeys.length > 0) {
14158
+ warn(`Ignoring ${strippedLspKeys.join(", ")} from project config ${projectConfigPath} (security: these LSP settings only honor user-level config)`);
14159
+ }
14160
+ const strippedTopLevelKeys = getStrippedTopLevelKeys(projectConfig);
14161
+ if (strippedTopLevelKeys.length > 0) {
14162
+ warn(`Ignoring ${strippedTopLevelKeys.join(", ")} from project config ${projectConfigPath} (security: these settings only honor user-level config — a project should not weaken security boundaries for the user)`);
14163
+ }
14103
14164
  config2 = mergeConfigs(config2, projectConfig);
14104
14165
  }
14105
14166
  return config2;
14106
14167
  }
14107
14168
 
14108
- // src/notifications.ts
14109
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
14169
+ // src/lsp-auto-install.ts
14170
+ import { spawn } from "node:child_process";
14171
+ import { createHash } from "node:crypto";
14172
+ import { createReadStream, statSync as statSync2 } from "node:fs";
14173
+
14174
+ // src/lsp-cache.ts
14175
+ import {
14176
+ closeSync,
14177
+ mkdirSync,
14178
+ openSync,
14179
+ readFileSync as readFileSync2,
14180
+ statSync,
14181
+ unlinkSync,
14182
+ writeFileSync
14183
+ } from "node:fs";
14184
+ import { homedir as homedir2 } from "node:os";
14110
14185
  import { join as join3 } from "node:path";
14111
- var WARNING_MARKER = "\uD83D\uDD27 AFT: ⚠️";
14112
- var WARNED_TOOLS_FILE = "warned_tools.json";
14113
- function sendIgnoredMessage(client, _sessionId, text) {
14114
- const typedClient = client;
14115
- if (typeof typedClient.ui?.notify !== "function")
14116
- return false;
14186
+ function aftCacheBase() {
14187
+ const override = process.env.AFT_CACHE_DIR;
14188
+ if (override && override.length > 0)
14189
+ return override;
14190
+ if (process.platform === "win32") {
14191
+ const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
14192
+ const base2 = localAppData || join3(homedir2(), "AppData", "Local");
14193
+ return join3(base2, "aft");
14194
+ }
14195
+ const base = process.env.XDG_CACHE_HOME || join3(homedir2(), ".cache");
14196
+ return join3(base, "aft");
14197
+ }
14198
+ function lspCacheRoot() {
14199
+ return join3(aftCacheBase(), "lsp-packages");
14200
+ }
14201
+ function lspPackageDir(npmPackage) {
14202
+ return join3(lspCacheRoot(), encodeURIComponent(npmPackage));
14203
+ }
14204
+ function lspBinaryPath(npmPackage, binary) {
14205
+ return join3(lspPackageDir(npmPackage), "node_modules", ".bin", binary);
14206
+ }
14207
+ function lspBinDir(npmPackage) {
14208
+ return join3(lspPackageDir(npmPackage), "node_modules", ".bin");
14209
+ }
14210
+ function isInstalled(npmPackage, binary) {
14211
+ for (const candidate of lspBinaryCandidates(binary)) {
14212
+ try {
14213
+ if (statSync(join3(lspBinDir(npmPackage), candidate)).isFile())
14214
+ return true;
14215
+ } catch {}
14216
+ }
14217
+ return false;
14218
+ }
14219
+ function lspBinaryCandidates(binary) {
14220
+ if (process.platform !== "win32")
14221
+ return [binary];
14222
+ return [binary, `${binary}.cmd`, `${binary}.exe`, `${binary}.bat`];
14223
+ }
14224
+ var INSTALLED_META_FILE = ".aft-installed";
14225
+ function writeInstalledMetaIn(installDir, version2, sha256) {
14117
14226
  try {
14118
- typedClient.ui.notify(text, "warning");
14119
- return true;
14227
+ mkdirSync(installDir, { recursive: true });
14228
+ const meta3 = {
14229
+ version: version2,
14230
+ installedAt: new Date().toISOString(),
14231
+ ...sha256 ? { sha256 } : {}
14232
+ };
14233
+ writeFileSync(join3(installDir, INSTALLED_META_FILE), JSON.stringify(meta3), "utf8");
14120
14234
  } catch (err) {
14121
- log(`[aft-pi] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
14122
- return false;
14235
+ log(`[lsp-cache] failed to write installed-meta in ${installDir}: ${err}`);
14123
14236
  }
14124
14237
  }
14125
- function readWarnedTools(storageDir) {
14238
+ function readInstalledMetaIn(installDir) {
14239
+ const path2 = join3(installDir, INSTALLED_META_FILE);
14126
14240
  try {
14127
- const warnedToolsPath = join3(storageDir, WARNED_TOOLS_FILE);
14128
- if (!existsSync2(warnedToolsPath))
14129
- return {};
14130
- const parsed = JSON.parse(readFileSync2(warnedToolsPath, "utf-8"));
14131
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
14132
- return {};
14133
- const warned = {};
14134
- for (const [key, version2] of Object.entries(parsed)) {
14135
- if (typeof version2 === "string") {
14136
- warned[key] = version2;
14137
- }
14138
- }
14139
- return warned;
14241
+ if (!statSync(path2).isFile())
14242
+ return null;
14243
+ const raw = readFileSync2(path2, "utf8");
14244
+ const parsed = JSON.parse(raw);
14245
+ if (typeof parsed.version !== "string" || parsed.version.length === 0)
14246
+ return null;
14247
+ return {
14248
+ version: parsed.version,
14249
+ installedAt: typeof parsed.installedAt === "string" ? parsed.installedAt : "",
14250
+ ...typeof parsed.sha256 === "string" && parsed.sha256.length > 0 ? { sha256: parsed.sha256 } : {}
14251
+ };
14140
14252
  } catch {
14141
- return {};
14253
+ return null;
14142
14254
  }
14143
14255
  }
14144
- function writeWarnedTools(storageDir, warned) {
14145
- try {
14146
- mkdirSync(storageDir, { recursive: true });
14147
- const warnedToolsPath = join3(storageDir, WARNED_TOOLS_FILE);
14148
- writeFileSync(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
14149
- `);
14150
- } catch {}
14151
- }
14152
- function warningKey(warning, projectRoot) {
14153
- return [
14154
- projectRoot ?? "_",
14155
- warning.kind,
14156
- warning.language ?? warning.server ?? "_",
14157
- warning.tool ?? warning.binary ?? "_",
14158
- warning.hint
14159
- ].map((part) => encodeURIComponent(part)).join(":");
14256
+ function writeInstalledMeta(packageKey, version2, sha256) {
14257
+ writeInstalledMetaIn(lspPackageDir(packageKey), version2, sha256);
14160
14258
  }
14161
- function warningTitle(warning) {
14162
- switch (warning.kind) {
14163
- case "formatter_not_installed":
14164
- return "Formatter is not installed";
14165
- case "checker_not_installed":
14166
- return "Checker is not installed";
14167
- case "lsp_binary_missing":
14168
- return "LSP binary is missing";
14169
- }
14259
+ function readInstalledMeta(packageKey) {
14260
+ return readInstalledMetaIn(lspPackageDir(packageKey));
14170
14261
  }
14171
- function formatConfigureWarning(warning) {
14172
- const details = [];
14173
- if (warning.language)
14174
- details.push(`language: ${warning.language}`);
14175
- if (warning.server)
14176
- details.push(`server: ${warning.server}`);
14177
- if (warning.tool)
14178
- details.push(`tool: ${warning.tool}`);
14179
- if (warning.binary && warning.binary !== warning.tool) {
14180
- details.push(`binary: ${warning.binary}`);
14181
- }
14182
- const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
14183
- return `${WARNING_MARKER} ${warningTitle(warning)}${suffix}
14184
- ${warning.hint}`;
14262
+ function lockPath(npmPackage) {
14263
+ return join3(lspPackageDir(npmPackage), ".aft-installing");
14185
14264
  }
14186
- async function deliverConfigureWarnings(opts, warnings) {
14187
- if (warnings.length === 0)
14188
- return;
14189
- const warned = readWarnedTools(opts.storageDir);
14190
- let changed = false;
14191
- for (const warning of warnings) {
14192
- const key = warningKey(warning, opts.projectRoot);
14193
- if (Object.hasOwn(warned, key))
14194
- continue;
14195
- if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning))) {
14196
- continue;
14265
+ var STALE_LOCK_MS = 30 * 60 * 1000;
14266
+ function acquireInstallLock(lockKey) {
14267
+ mkdirSync(lspPackageDir(lockKey), { recursive: true });
14268
+ const lock = lockPath(lockKey);
14269
+ const tryClaim = () => {
14270
+ try {
14271
+ const fd = openSync(lock, "wx");
14272
+ try {
14273
+ writeFileSync(fd, `${process.pid}
14274
+ ${new Date().toISOString()}
14275
+ `);
14276
+ } finally {
14277
+ closeSync(fd);
14278
+ }
14279
+ return true;
14280
+ } catch (err) {
14281
+ const code = err.code;
14282
+ if (code === "EEXIST")
14283
+ return false;
14284
+ warn(`[lsp] unexpected error acquiring install lock for ${lockKey}: ${err}`);
14285
+ return false;
14197
14286
  }
14198
- warned[key] = opts.pluginVersion;
14199
- changed = true;
14287
+ };
14288
+ if (tryClaim())
14289
+ return true;
14290
+ let owningPid = null;
14291
+ let lockMtimeMs = 0;
14292
+ try {
14293
+ const raw = readFileSync2(lock, "utf8");
14294
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
14295
+ const parsed = Number.parseInt(firstLine, 10);
14296
+ if (Number.isFinite(parsed) && parsed > 0)
14297
+ owningPid = parsed;
14298
+ lockMtimeMs = statSync(lock).mtimeMs;
14299
+ } catch {
14300
+ return tryClaim();
14200
14301
  }
14201
- if (changed) {
14202
- writeWarnedTools(opts.storageDir, warned);
14302
+ const age = Date.now() - lockMtimeMs;
14303
+ const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS;
14304
+ const skipLiveness = process.platform === "win32";
14305
+ const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive(owningPid);
14306
+ if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
14307
+ return false;
14203
14308
  }
14309
+ log(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
14310
+ try {
14311
+ unlinkSync(lock);
14312
+ } catch {}
14313
+ return tryClaim();
14204
14314
  }
14205
-
14206
- // src/onnx-runtime.ts
14207
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, unlinkSync } from "node:fs";
14208
- import { join as join4 } from "node:path";
14209
- var ORT_VERSION = "1.24.4";
14210
- var ORT_REPO = "microsoft/onnxruntime";
14211
- var ORT_PLATFORM_MAP = {
14212
- darwin: {
14213
- arm64: {
14214
- assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
14215
- libName: "libonnxruntime.dylib",
14216
- archiveType: "tgz"
14315
+ function isProcessAlive(pid) {
14316
+ try {
14317
+ process.kill(pid, 0);
14318
+ return true;
14319
+ } catch (err) {
14320
+ const code = err.code;
14321
+ if (code === "ESRCH")
14322
+ return false;
14323
+ return true;
14324
+ }
14325
+ }
14326
+ function releaseInstallLock(lockKey) {
14327
+ const lock = lockPath(lockKey);
14328
+ try {
14329
+ let owningPid = null;
14330
+ try {
14331
+ const raw = readFileSync2(lock, "utf8");
14332
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
14333
+ const parsed = Number.parseInt(firstLine, 10);
14334
+ if (Number.isFinite(parsed) && parsed > 0)
14335
+ owningPid = parsed;
14336
+ } catch (readErr) {
14337
+ const code = readErr.code;
14338
+ if (code === "ENOENT")
14339
+ return;
14340
+ warn(`[lsp] could not read install lock for ${lockKey} during release: ${readErr}`);
14341
+ return;
14217
14342
  }
14218
- },
14219
- linux: {
14220
- x64: {
14221
- assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
14222
- libName: "libonnxruntime.so",
14223
- archiveType: "tgz"
14224
- },
14225
- arm64: {
14226
- assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
14227
- libName: "libonnxruntime.so",
14228
- archiveType: "tgz"
14343
+ if (owningPid !== process.pid) {
14344
+ log(`[lsp] not releasing install lock for ${lockKey}: owned by pid ${owningPid ?? "unknown"} (we are ${process.pid})`);
14345
+ return;
14229
14346
  }
14230
- },
14231
- win32: {
14232
- x64: {
14233
- assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
14234
- libName: "onnxruntime.dll",
14235
- archiveType: "zip"
14236
- },
14237
- arm64: {
14238
- assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
14239
- libName: "onnxruntime.dll",
14240
- archiveType: "zip"
14347
+ try {
14348
+ unlinkSync(lock);
14349
+ } catch (unlinkErr) {
14350
+ const code = unlinkErr.code;
14351
+ if (code !== "ENOENT") {
14352
+ warn(`[lsp] failed to release install lock for ${lockKey}: ${unlinkErr}`);
14353
+ }
14241
14354
  }
14355
+ } catch (err) {
14356
+ warn(`[lsp] unexpected error releasing install lock for ${lockKey}: ${err}`);
14242
14357
  }
14243
- };
14244
- function getPlatformInfo() {
14245
- const platformMap = ORT_PLATFORM_MAP[process.platform];
14246
- if (!platformMap)
14247
- return null;
14248
- return platformMap[process.arch] || null;
14249
- }
14250
- function getManualInstallHint() {
14251
- if (process.platform === "darwin" && process.arch === "x64") {
14252
- return "brew install onnxruntime";
14253
- }
14254
- if (process.platform === "linux") {
14255
- return "apt install libonnxruntime or download from https://github.com/microsoft/onnxruntime/releases";
14256
- }
14257
- return "Download from https://github.com/microsoft/onnxruntime/releases";
14258
14358
  }
14259
- async function ensureOnnxRuntime(storageDir) {
14260
- const info = getPlatformInfo();
14261
- const ortDir = join4(storageDir, "onnxruntime", ORT_VERSION);
14262
- const libPath = join4(ortDir, info?.libName ?? "libonnxruntime.dylib");
14263
- if (existsSync3(libPath)) {
14264
- log(`ONNX Runtime found at ${ortDir}`);
14265
- return ortDir;
14266
- }
14267
- const systemPath = findSystemOnnxRuntime(info?.libName);
14268
- if (systemPath) {
14269
- log(`ONNX Runtime found at system path: ${systemPath}`);
14270
- return systemPath;
14271
- }
14272
- if (!info) {
14273
- warn(`ONNX Runtime auto-download not available for ${process.platform}/${process.arch}. Install manually: ${getManualInstallHint()}`);
14359
+ async function withInstallLock(lockKey, task) {
14360
+ if (!acquireInstallLock(lockKey))
14274
14361
  return null;
14362
+ try {
14363
+ return await task();
14364
+ } finally {
14365
+ releaseInstallLock(lockKey);
14275
14366
  }
14276
- return downloadOnnxRuntime(info, ortDir);
14277
14367
  }
14278
- function findSystemOnnxRuntime(libName) {
14279
- if (!libName)
14368
+ var VERSION_CHECK_FILE = ".aft-version-check";
14369
+ function readVersionCheck(npmPackage) {
14370
+ const file2 = join3(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
14371
+ try {
14372
+ const raw = readFileSync2(file2, "utf8");
14373
+ const parsed = JSON.parse(raw);
14374
+ if (typeof parsed.last_checked === "string") {
14375
+ return {
14376
+ last_checked: parsed.last_checked,
14377
+ latest_eligible: typeof parsed.latest_eligible === "string" ? parsed.latest_eligible : null
14378
+ };
14379
+ }
14380
+ return null;
14381
+ } catch {
14280
14382
  return null;
14281
- const searchPaths = [];
14282
- if (process.platform === "darwin") {
14283
- searchPaths.push("/opt/homebrew/lib", "/usr/local/lib");
14284
- } else if (process.platform === "linux") {
14285
- searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
14286
14383
  }
14287
- for (const dir of searchPaths) {
14288
- if (existsSync3(join4(dir, libName))) {
14289
- return dir;
14290
- }
14384
+ }
14385
+ function writeVersionCheck(npmPackage, latest) {
14386
+ mkdirSync(lspPackageDir(npmPackage), { recursive: true });
14387
+ const file2 = join3(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
14388
+ const record2 = {
14389
+ last_checked: new Date().toISOString(),
14390
+ latest_eligible: latest
14391
+ };
14392
+ writeFileSync(file2, JSON.stringify(record2, null, 2));
14393
+ }
14394
+ function shouldRecheckVersion(record2, weeklyCheckIntervalMs = 7 * 24 * 60 * 60 * 1000) {
14395
+ if (!record2)
14396
+ return true;
14397
+ const age = Date.now() - new Date(record2.last_checked).getTime();
14398
+ if (Number.isNaN(age) || age < 0)
14399
+ return true;
14400
+ return age >= weeklyCheckIntervalMs;
14401
+ }
14402
+
14403
+ // src/lsp-github-probe.ts
14404
+ function pickEligibleRelease(releases, graceDays, now = Date.now()) {
14405
+ const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
14406
+ const candidates = releases.filter((r) => !r.draft && !r.prerelease && typeof r.published_at === "string").map((r) => {
14407
+ const ts = Date.parse(r.published_at);
14408
+ return { release: r, ts };
14409
+ }).filter((c) => !Number.isNaN(c.ts)).sort((a, b) => b.ts - a.ts);
14410
+ const eligible = candidates.filter((c) => c.ts <= cutoff);
14411
+ const blockedByGrace = candidates.length > 0 && eligible.length === 0;
14412
+ const chosen = eligible[0]?.release;
14413
+ if (!chosen) {
14414
+ return { tag: null, assets: [], blockedByGrace };
14291
14415
  }
14292
- return null;
14416
+ return {
14417
+ tag: chosen.tag_name,
14418
+ assets: (chosen.assets ?? []).map((a) => ({
14419
+ name: a.name,
14420
+ url: a.browser_download_url,
14421
+ size: a.size
14422
+ })),
14423
+ blockedByGrace: false
14424
+ };
14293
14425
  }
14294
- async function downloadOnnxRuntime(info, targetDir) {
14295
- const url2 = `https://github.com/${ORT_REPO}/releases/download/v${ORT_VERSION}/${info.assetName}.${info.archiveType === "tgz" ? "tgz" : "zip"}`;
14296
- log(`Downloading ONNX Runtime v${ORT_VERSION} for ${process.platform}/${process.arch}...`);
14426
+ async function probeGithubReleases(githubRepo, graceDays, fetchImpl = fetch) {
14427
+ const url2 = `https://api.github.com/repos/${githubRepo}/releases?per_page=30`;
14297
14428
  try {
14298
- const tmpDir = `${targetDir}.tmp.${process.pid}`;
14299
- mkdirSync2(tmpDir, { recursive: true });
14300
- const archivePath = join4(tmpDir, `onnxruntime.${info.archiveType}`);
14301
- const { execFileSync } = await import("node:child_process");
14302
- execFileSync("curl", ["-fsSL", url2, "-o", archivePath], {
14303
- stdio: "pipe",
14304
- timeout: 120000
14305
- });
14306
- if (info.archiveType === "tgz") {
14307
- execFileSync("tar", ["xzf", archivePath, "-C", tmpDir], { stdio: "pipe" });
14308
- } else {
14309
- await extractZipArchive(archivePath, tmpDir);
14429
+ const headers = { accept: "application/vnd.github+json" };
14430
+ if (process.env.GITHUB_TOKEN) {
14431
+ headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
14310
14432
  }
14311
- const extractedDir = join4(tmpDir, info.assetName, "lib");
14312
- if (!existsSync3(extractedDir)) {
14313
- throw new Error(`Expected directory not found: ${extractedDir}`);
14433
+ const res = await fetchImpl(url2, {
14434
+ headers,
14435
+ signal: AbortSignal.timeout(1e4)
14436
+ });
14437
+ if (!res.ok) {
14438
+ warn(`[lsp] github releases probe failed for ${githubRepo}: HTTP ${res.status}`);
14439
+ return null;
14314
14440
  }
14315
- mkdirSync2(targetDir, { recursive: true });
14316
- const libFiles = readdirSync(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
14317
- const { lstatSync, symlinkSync, readlinkSync, copyFileSync: cpFile } = await import("node:fs");
14318
- const realFiles = [];
14319
- const symlinks = [];
14320
- for (const libFile of libFiles) {
14321
- const src = join4(extractedDir, libFile);
14322
- try {
14323
- const stat = lstatSync(src);
14324
- log(`ORT extract: ${libFile} — isSymlink=${stat.isSymbolicLink()}, isFile=${stat.isFile()}, size=${stat.size}`);
14325
- if (stat.isSymbolicLink()) {
14326
- symlinks.push({ name: libFile, target: readlinkSync(src) });
14327
- } else {
14328
- realFiles.push(libFile);
14329
- }
14330
- } catch (e) {
14331
- log(`ORT extract: ${libFile} — stat failed: ${e}`);
14332
- realFiles.push(libFile);
14333
- }
14441
+ const json2 = await res.json();
14442
+ if (!Array.isArray(json2)) {
14443
+ warn(`[lsp] unexpected response shape from github releases for ${githubRepo}`);
14444
+ return null;
14334
14445
  }
14335
- for (const libFile of realFiles) {
14336
- const src = join4(extractedDir, libFile);
14337
- const dst = join4(targetDir, libFile);
14446
+ return pickEligibleRelease(json2, graceDays);
14447
+ } catch (err) {
14448
+ warn(`[lsp] github releases probe failed for ${githubRepo}: ${err}`);
14449
+ return null;
14450
+ }
14451
+ }
14452
+ var SAFE_VERSION_RE = /^[A-Za-z0-9._+-]+$/;
14453
+ function assertSafeVersion(version2) {
14454
+ if (!SAFE_VERSION_RE.test(version2)) {
14455
+ throw new Error(`unsafe version/tag string ${JSON.stringify(version2)}: must match ${SAFE_VERSION_RE.source}`);
14456
+ }
14457
+ }
14458
+ function isSafeVersion(version2) {
14459
+ return typeof version2 === "string" && version2.length > 0 && SAFE_VERSION_RE.test(version2);
14460
+ }
14461
+ function stripTagV(tag) {
14462
+ assertSafeVersion(tag);
14463
+ return tag.startsWith("v") ? tag.slice(1) : tag;
14464
+ }
14465
+
14466
+ // src/lsp-npm-table.ts
14467
+ var NPM_LSP_TABLE = [
14468
+ {
14469
+ id: "typescript",
14470
+ npm: "typescript-language-server",
14471
+ binary: "typescript-language-server",
14472
+ extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
14473
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
14474
+ },
14475
+ {
14476
+ id: "python",
14477
+ npm: "pyright",
14478
+ binary: "pyright-langserver",
14479
+ extensions: ["py", "pyi"],
14480
+ rootMarkers: ["pyproject.toml", "pyrightconfig.json", "requirements.txt"]
14481
+ },
14482
+ {
14483
+ id: "yaml",
14484
+ npm: "yaml-language-server",
14485
+ binary: "yaml-language-server",
14486
+ extensions: ["yaml", "yml"]
14487
+ },
14488
+ {
14489
+ id: "bash",
14490
+ npm: "bash-language-server",
14491
+ binary: "bash-language-server",
14492
+ extensions: ["sh", "bash", "zsh"]
14493
+ },
14494
+ {
14495
+ id: "dockerfile",
14496
+ npm: "dockerfile-language-server-nodejs",
14497
+ binary: "docker-langserver",
14498
+ extensions: ["dockerfile"],
14499
+ rootMarkers: ["Dockerfile", "dockerfile"]
14500
+ },
14501
+ {
14502
+ id: "vue",
14503
+ npm: "@vue/language-server",
14504
+ binary: "vue-language-server",
14505
+ extensions: ["vue"]
14506
+ },
14507
+ {
14508
+ id: "astro",
14509
+ npm: "@astrojs/language-server",
14510
+ binary: "astro-ls",
14511
+ extensions: ["astro"]
14512
+ },
14513
+ {
14514
+ id: "svelte",
14515
+ npm: "svelte-language-server",
14516
+ binary: "svelteserver",
14517
+ extensions: ["svelte"]
14518
+ },
14519
+ {
14520
+ id: "php-intelephense",
14521
+ npm: "intelephense",
14522
+ binary: "intelephense",
14523
+ extensions: ["php"]
14524
+ },
14525
+ {
14526
+ id: "biome",
14527
+ npm: "@biomejs/biome",
14528
+ binary: "biome",
14529
+ extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts", "json", "jsonc"],
14530
+ rootMarkers: ["biome.json", "biome.jsonc"]
14531
+ }
14532
+ ];
14533
+
14534
+ // src/lsp-project-relevance.ts
14535
+ import { existsSync as existsSync2, readdirSync } from "node:fs";
14536
+ import { join as join4 } from "node:path";
14537
+ var MAX_WALK_DIRS = 200;
14538
+ var MAX_WALK_DEPTH = 4;
14539
+ var NOISE_DIRS = new Set([
14540
+ ".git",
14541
+ ".next",
14542
+ ".venv",
14543
+ "__pycache__",
14544
+ "build",
14545
+ "dist",
14546
+ "node_modules",
14547
+ "target"
14548
+ ]);
14549
+ function hasRootMarker(projectRoot, rootMarkers) {
14550
+ if (!rootMarkers)
14551
+ return false;
14552
+ for (const marker of rootMarkers) {
14553
+ if (existsSync2(join4(projectRoot, marker)))
14554
+ return true;
14555
+ }
14556
+ return false;
14557
+ }
14558
+ function relevantExtensionsInProject(projectRoot, extToServer) {
14559
+ const wanted = new Set(Object.keys(extToServer).map((ext) => ext.toLowerCase()));
14560
+ const found = new Set;
14561
+ if (wanted.size === 0)
14562
+ return found;
14563
+ const queue = [{ dir: projectRoot, depth: 0 }];
14564
+ let visitedDirs = 0;
14565
+ while (queue.length > 0 && visitedDirs < MAX_WALK_DIRS) {
14566
+ const current = queue.shift();
14567
+ if (!current)
14568
+ break;
14569
+ visitedDirs += 1;
14570
+ let entries;
14571
+ try {
14572
+ entries = readdirSync(current.dir, { withFileTypes: true });
14573
+ } catch {
14574
+ continue;
14575
+ }
14576
+ for (const entry of entries) {
14577
+ if (entry.isDirectory()) {
14578
+ if (current.depth < MAX_WALK_DEPTH && !NOISE_DIRS.has(entry.name.toLowerCase())) {
14579
+ queue.push({ dir: join4(current.dir, entry.name), depth: current.depth + 1 });
14580
+ }
14581
+ continue;
14582
+ }
14583
+ if (!entry.isFile())
14584
+ continue;
14585
+ const ext = extensionOf(entry.name);
14586
+ if (ext && wanted.has(ext))
14587
+ found.add(ext);
14588
+ }
14589
+ }
14590
+ return found;
14591
+ }
14592
+ function extensionOf(fileName) {
14593
+ const dot = fileName.lastIndexOf(".");
14594
+ if (dot < 0 || dot === fileName.length - 1)
14595
+ return null;
14596
+ return fileName.slice(dot + 1).toLowerCase();
14597
+ }
14598
+
14599
+ // src/lsp-registry-probe.ts
14600
+ var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
14601
+ function pickEligibleVersion(response, graceDays, now = Date.now()) {
14602
+ const times = response.time || {};
14603
+ const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
14604
+ const candidates = [];
14605
+ for (const [version2, publishedAt] of Object.entries(times)) {
14606
+ if (version2 === "created" || version2 === "modified")
14607
+ continue;
14608
+ if (version2.includes("-"))
14609
+ continue;
14610
+ if (typeof publishedAt !== "string")
14611
+ continue;
14612
+ const ts = Date.parse(publishedAt);
14613
+ if (Number.isNaN(ts))
14614
+ continue;
14615
+ candidates.push({ version: version2, publishedAt, ts });
14616
+ }
14617
+ candidates.sort((a, b) => b.ts - a.ts);
14618
+ const eligible = candidates.filter((c) => c.ts <= cutoff);
14619
+ const blockedByGrace = candidates.length > 0 && eligible.length === 0;
14620
+ return {
14621
+ version: eligible[0]?.version ?? null,
14622
+ blockedByGrace,
14623
+ eligible: eligible.map(({ version: version2, publishedAt }) => ({ version: version2, publishedAt }))
14624
+ };
14625
+ }
14626
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
14627
+ const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
14628
+ const url2 = `${NPM_REGISTRY_BASE}/${encoded}`;
14629
+ try {
14630
+ const res = await fetchImpl(url2, {
14631
+ headers: { accept: "application/json" },
14632
+ signal: AbortSignal.timeout(1e4)
14633
+ });
14634
+ if (!res.ok) {
14635
+ warn(`[lsp] registry probe failed for ${npmPackage}: HTTP ${res.status}`);
14636
+ return null;
14637
+ }
14638
+ const json2 = await res.json();
14639
+ return pickEligibleVersion(json2, graceDays);
14640
+ } catch (err) {
14641
+ warn(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
14642
+ return null;
14643
+ }
14644
+ }
14645
+
14646
+ // src/lsp-auto-install.ts
14647
+ function isProjectRelevant(spec, projectRoot, projectExtensions) {
14648
+ if (hasRootMarker(projectRoot, spec.rootMarkers))
14649
+ return true;
14650
+ const extensions = projectExtensions();
14651
+ return spec.extensions.some((ext) => extensions.has(ext.toLowerCase()));
14652
+ }
14653
+ var npmExtToServerIds = buildExtensionMap(NPM_LSP_TABLE);
14654
+ function buildExtensionMap(specs) {
14655
+ const byExt = {};
14656
+ for (const spec of specs) {
14657
+ for (const ext of spec.extensions) {
14658
+ const key = ext.toLowerCase();
14659
+ byExt[key] ??= [];
14660
+ byExt[key].push(spec.id);
14661
+ }
14662
+ }
14663
+ return byExt;
14664
+ }
14665
+ var inFlightAutoInstalls = new Set;
14666
+ function trackInFlightAutoInstall(controller, promise2) {
14667
+ const entry = { controller, promise: promise2 };
14668
+ inFlightAutoInstalls.add(entry);
14669
+ promise2.then(() => inFlightAutoInstalls.delete(entry), () => inFlightAutoInstalls.delete(entry));
14670
+ return promise2;
14671
+ }
14672
+ async function abortInFlightAutoInstalls() {
14673
+ const installs = Array.from(inFlightAutoInstalls);
14674
+ for (const install of installs) {
14675
+ install.controller.abort();
14676
+ }
14677
+ await Promise.allSettled(installs.map((install) => install.promise));
14678
+ }
14679
+ async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
14680
+ const pinned = config2.versions[spec.npm];
14681
+ if (pinned) {
14682
+ assertSafeVersion(pinned);
14683
+ return { version: pinned, pinned: true, probe: null };
14684
+ }
14685
+ const cached2 = readVersionCheck(spec.npm);
14686
+ const weeklyMs = config2.graceDays * 24 * 60 * 60 * 1000;
14687
+ const cachedSafe = isSafeVersion(cached2?.latest_eligible ?? null);
14688
+ if (cached2 && !shouldRecheckVersion(cached2, weeklyMs) && cachedSafe) {
14689
+ return { version: cached2.latest_eligible, pinned: false, probe: null };
14690
+ }
14691
+ const probe = await probeRegistry(spec.npm, config2.graceDays, fetchImpl);
14692
+ if (!probe) {
14693
+ return {
14694
+ version: cachedSafe ? cached2?.latest_eligible ?? null : null,
14695
+ pinned: false,
14696
+ probe: null
14697
+ };
14698
+ }
14699
+ writeVersionCheck(spec.npm, probe.version);
14700
+ return { version: probe.version, pinned: false, probe };
14701
+ }
14702
+ function runInstall(spec, version2, cwd, signal) {
14703
+ return new Promise((resolve) => {
14704
+ const target = `${spec.npm}@${version2}`;
14705
+ log(`[lsp] installing ${target} to ${cwd}`);
14706
+ if (signal?.aborted) {
14707
+ warn(`[lsp] install ${target} aborted before spawn`);
14708
+ resolve(false);
14709
+ return;
14710
+ }
14711
+ const child = spawn("bun", ["add", target, "--cwd", cwd, "--ignore-scripts", "--silent"], {
14712
+ stdio: ["ignore", "pipe", "pipe"]
14713
+ });
14714
+ child.unref();
14715
+ let stderrBuf = "";
14716
+ let settled = false;
14717
+ let killTimer = null;
14718
+ const cleanup = () => {
14719
+ signal?.removeEventListener("abort", onAbort);
14720
+ if (killTimer)
14721
+ clearTimeout(killTimer);
14722
+ };
14723
+ const finish = (ok) => {
14724
+ if (settled)
14725
+ return;
14726
+ settled = true;
14727
+ cleanup();
14728
+ resolve(ok);
14729
+ };
14730
+ const onAbort = () => {
14731
+ warn(`[lsp] install ${target} aborted during shutdown`);
14732
+ child.kill("SIGTERM");
14733
+ killTimer = setTimeout(() => {
14734
+ if (!settled)
14735
+ child.kill("SIGKILL");
14736
+ }, 5000);
14737
+ killTimer.unref?.();
14738
+ };
14739
+ signal?.addEventListener("abort", onAbort, { once: true });
14740
+ if (signal?.aborted)
14741
+ onAbort();
14742
+ child.stdout?.on("data", () => {});
14743
+ child.stderr?.on("data", (chunk) => {
14744
+ const text = String(chunk);
14745
+ stderrBuf += text;
14746
+ if (stderrBuf.length > 4096) {
14747
+ stderrBuf = stderrBuf.slice(stderrBuf.length - 4096);
14748
+ }
14749
+ });
14750
+ child.on("error", (err) => {
14751
+ error48(`[lsp] install ${target} failed to spawn: ${err}`);
14752
+ finish(false);
14753
+ });
14754
+ child.on("exit", (code) => {
14755
+ if (code === 0) {
14756
+ log(`[lsp] installed ${target}`);
14757
+ finish(true);
14758
+ } else {
14759
+ error48(`[lsp] install ${target} exited with code ${code}; last stderr:
14760
+ ${stderrBuf.trim()}`);
14761
+ finish(false);
14762
+ }
14763
+ });
14764
+ });
14765
+ }
14766
+ async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
14767
+ const outcome = await withInstallLock(spec.npm, async () => {
14768
+ const { version: version2, probe } = await resolveTargetVersion(spec, config2, fetchImpl);
14769
+ if (!version2) {
14770
+ const installed = isInstalled(spec.npm, spec.binary);
14771
+ if (installed) {
14772
+ warn(`[lsp] no eligible version of ${spec.npm} (grace=${config2.graceDays}d); keeping existing install`);
14773
+ return { started: false, reason: "kept existing install" };
14774
+ }
14775
+ const blocked = probe?.blockedByGrace ? `all versions are within ${config2.graceDays}-day grace window` : "registry probe failed";
14776
+ warn(`[lsp] skipping ${spec.npm}: ${blocked}`);
14777
+ return { started: false, reason: blocked };
14778
+ }
14779
+ if (isInstalled(spec.npm, spec.binary)) {
14780
+ const installedMeta = readInstalledMeta(spec.npm);
14781
+ if (installedMeta && installedMeta.version === version2) {
14782
+ if (installedMeta.sha256) {
14783
+ const currentHash = await hashInstalledBinary(spec).catch((err) => {
14784
+ warn(`[lsp] could not hash existing ${spec.npm} binary for TOFU check: ${err}`);
14785
+ return null;
14786
+ });
14787
+ if (currentHash && currentHash !== installedMeta.sha256) {
14788
+ error48(`[lsp] ${spec.npm}@${version2}: TOFU sha256 mismatch — refusing to use ` + `tampered binary. Recorded ${installedMeta.sha256}, current ${currentHash}. ` + `Run \`aft doctor --clear\` to re-install from scratch.`);
14789
+ return {
14790
+ started: false,
14791
+ reason: `TOFU sha256 mismatch on ${spec.npm}@${version2} — see plugin log`
14792
+ };
14793
+ }
14794
+ }
14795
+ return { started: false, reason: "already installed" };
14796
+ }
14797
+ if (installedMeta) {
14798
+ log(`[lsp] reinstalling ${spec.npm}: cached ${installedMeta.version} ≠ target ${version2}`);
14799
+ } else {
14800
+ log(`[lsp] reinstalling ${spec.npm}@${version2}: no installed-version metadata recorded`);
14801
+ }
14802
+ }
14803
+ const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), signal).catch((err) => {
14804
+ error48(`[lsp] background install ${spec.npm} crashed: ${err}`);
14805
+ return false;
14806
+ });
14807
+ if (!ok) {
14808
+ return { started: true, reason: "install failed (see plugin log)" };
14809
+ }
14810
+ const installedHash = await hashInstalledBinary(spec).catch((err) => {
14811
+ warn(`[lsp] could not hash newly-installed ${spec.npm} binary: ${err}`);
14812
+ return null;
14813
+ });
14814
+ if (installedHash) {
14815
+ log(`[lsp] ${spec.npm}@${version2} installed sha256=${installedHash}`);
14816
+ }
14817
+ writeInstalledMeta(spec.npm, version2, installedHash ?? undefined);
14818
+ return { started: true };
14819
+ });
14820
+ if (outcome === null) {
14821
+ return { started: false, reason: "another install in progress" };
14822
+ }
14823
+ return outcome;
14824
+ }
14825
+ function cachedPackageDir(npmPackage) {
14826
+ return lspBinDir(npmPackage).replace(/[\\/]node_modules[\\/]\.bin[\\/]?$/, "");
14827
+ }
14828
+ function hashInstalledBinary(spec) {
14829
+ return new Promise((resolve, reject) => {
14830
+ const candidates = process.platform === "win32" ? [
14831
+ lspBinaryPath(spec.npm, spec.binary),
14832
+ lspBinaryPath(spec.npm, `${spec.binary}.cmd`),
14833
+ lspBinaryPath(spec.npm, `${spec.binary}.exe`),
14834
+ lspBinaryPath(spec.npm, `${spec.binary}.bat`)
14835
+ ] : [lspBinaryPath(spec.npm, spec.binary)];
14836
+ let pathToHash = null;
14837
+ for (const p of candidates) {
14838
+ try {
14839
+ if (statSync2(p).isFile()) {
14840
+ pathToHash = p;
14841
+ break;
14842
+ }
14843
+ } catch {}
14844
+ }
14845
+ if (!pathToHash) {
14846
+ reject(new Error(`installed binary not found at any of: ${candidates.join(", ")}`));
14847
+ return;
14848
+ }
14849
+ const hash2 = createHash("sha256");
14850
+ const stream = createReadStream(pathToHash);
14851
+ stream.on("error", reject);
14852
+ stream.on("data", (chunk) => hash2.update(chunk));
14853
+ stream.on("end", () => resolve(hash2.digest("hex")));
14854
+ });
14855
+ }
14856
+ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
14857
+ const cachedBinDirs = [];
14858
+ const skipped = [];
14859
+ const installPromises = [];
14860
+ let installsStarted = 0;
14861
+ let projectExtensions = null;
14862
+ const getProjectExtensions = () => {
14863
+ projectExtensions ??= relevantExtensionsInProject(projectRoot, npmExtToServerIds);
14864
+ return projectExtensions;
14865
+ };
14866
+ for (const spec of NPM_LSP_TABLE) {
14867
+ if (isInstalled(spec.npm, spec.binary)) {
14868
+ cachedBinDirs.push(lspBinDir(spec.npm));
14869
+ }
14870
+ if (config2.disabled.has(spec.id)) {
14871
+ skipped.push({ id: spec.id, reason: "disabled by config" });
14872
+ continue;
14873
+ }
14874
+ if (!config2.autoInstall) {
14875
+ skipped.push({ id: spec.id, reason: "auto_install: false" });
14876
+ continue;
14877
+ }
14878
+ if (!isProjectRelevant(spec, projectRoot, getProjectExtensions)) {
14879
+ skipped.push({ id: spec.id, reason: "not relevant to project" });
14880
+ continue;
14881
+ }
14882
+ installsStarted += 1;
14883
+ const controller = new AbortController;
14884
+ const promise2 = ensureServerInstalled(spec, config2, fetchImpl, controller.signal).then((outcome) => {
14885
+ if (!outcome.started)
14886
+ installsStarted -= 1;
14887
+ if (outcome.reason && outcome.reason !== "already installed") {
14888
+ skipped.push({ id: spec.id, reason: outcome.reason });
14889
+ }
14890
+ }, (err) => {
14891
+ installsStarted -= 1;
14892
+ const reason = err instanceof Error ? err.message : String(err);
14893
+ skipped.push({ id: spec.id, reason: `install error: ${reason}` });
14894
+ error48(`[lsp] background install ${spec.npm} promise rejected: ${reason}`);
14895
+ });
14896
+ installPromises.push(trackInFlightAutoInstall(controller, promise2));
14897
+ }
14898
+ return {
14899
+ cachedBinDirs,
14900
+ get installsStarted() {
14901
+ return installsStarted;
14902
+ },
14903
+ skipped,
14904
+ installsComplete: Promise.all(installPromises).then(() => {})
14905
+ };
14906
+ }
14907
+
14908
+ // src/lsp-github-install.ts
14909
+ import { execFileSync } from "node:child_process";
14910
+ import { createHash as createHash2, randomBytes } from "node:crypto";
14911
+ import {
14912
+ copyFileSync,
14913
+ createReadStream as createReadStream2,
14914
+ createWriteStream,
14915
+ existsSync as existsSync3,
14916
+ lstatSync,
14917
+ mkdirSync as mkdirSync2,
14918
+ readdirSync as readdirSync2,
14919
+ readlinkSync,
14920
+ realpathSync,
14921
+ renameSync,
14922
+ rmSync,
14923
+ statSync as statSync3,
14924
+ unlinkSync as unlinkSync2
14925
+ } from "node:fs";
14926
+ import { dirname, join as join5, relative, resolve } from "node:path";
14927
+ import { Readable } from "node:stream";
14928
+ import { pipeline } from "node:stream/promises";
14929
+
14930
+ // src/lsp-github-table.ts
14931
+ function exe(platform, name) {
14932
+ return platform === "win32" ? `${name}.exe` : name;
14933
+ }
14934
+ var CLANGD = {
14935
+ id: "clangd",
14936
+ githubRepo: "clangd/clangd",
14937
+ binary: "clangd",
14938
+ resolveAsset: (platform, _arch, version2) => {
14939
+ const platformName = platform === "darwin" ? "mac" : platform === "linux" ? "linux" : "windows";
14940
+ return { name: `clangd-${platformName}-${version2}.zip`, archive: "zip" };
14941
+ },
14942
+ binaryPathInArchive: (platform, _arch, version2) => `clangd_${version2}/bin/${exe(platform, "clangd")}`
14943
+ };
14944
+ var LUA_LS = {
14945
+ id: "lua-ls",
14946
+ githubRepo: "LuaLS/lua-language-server",
14947
+ binary: "lua-language-server",
14948
+ resolveAsset: (platform, arch, version2) => {
14949
+ const ext = platform === "win32" ? "zip" : "tar.gz";
14950
+ const platformName = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : "win32";
14951
+ const archName = arch === "arm64" ? "arm64" : "x64";
14952
+ return {
14953
+ name: `lua-language-server-${version2}-${platformName}-${archName}.${ext}`,
14954
+ archive: ext
14955
+ };
14956
+ },
14957
+ binaryPathInArchive: (platform, _arch, _version) => `bin/${exe(platform, "lua-language-server")}`
14958
+ };
14959
+ var ZLS = {
14960
+ id: "zls",
14961
+ githubRepo: "zigtools/zls",
14962
+ binary: "zls",
14963
+ resolveAsset: (platform, arch, _version) => {
14964
+ const ext = platform === "win32" ? "zip" : "tar.xz";
14965
+ const archName = arch === "arm64" ? "aarch64" : "x86_64";
14966
+ const platformName = platform === "darwin" ? "macos" : platform === "linux" ? "linux" : "windows";
14967
+ return { name: `zls-${archName}-${platformName}.${ext}`, archive: ext };
14968
+ },
14969
+ binaryPathInArchive: (platform, _arch, _version) => exe(platform, "zls")
14970
+ };
14971
+ var TINYMIST = {
14972
+ id: "tinymist",
14973
+ githubRepo: "Myriad-Dreamin/tinymist",
14974
+ binary: "tinymist",
14975
+ resolveAsset: (platform, arch, _version) => {
14976
+ const archName = arch === "arm64" ? "aarch64" : "x86_64";
14977
+ const triple = platform === "darwin" ? "apple-darwin" : platform === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
14978
+ const ext = platform === "win32" ? "zip" : "tar.gz";
14979
+ return { name: `tinymist-${archName}-${triple}.${ext}`, archive: ext };
14980
+ },
14981
+ binaryPathInArchive: (platform, _arch, _version) => exe(platform, "tinymist")
14982
+ };
14983
+ var TEXLAB = {
14984
+ id: "texlab",
14985
+ githubRepo: "latex-lsp/texlab",
14986
+ binary: "texlab",
14987
+ resolveAsset: (platform, arch, _version) => {
14988
+ const archName = arch === "arm64" ? "aarch64" : "x86_64";
14989
+ const platformName = platform === "darwin" ? "macos" : platform === "linux" ? "linux" : "windows";
14990
+ const ext = platform === "win32" ? "zip" : "tar.gz";
14991
+ return { name: `texlab-${archName}-${platformName}.${ext}`, archive: ext };
14992
+ },
14993
+ binaryPathInArchive: (platform, _arch, _version) => exe(platform, "texlab")
14994
+ };
14995
+ var GITHUB_LSP_TABLE = [
14996
+ CLANGD,
14997
+ LUA_LS,
14998
+ ZLS,
14999
+ TINYMIST,
15000
+ TEXLAB
15001
+ ];
15002
+ function detectHostPlatform() {
15003
+ const platform = process.platform;
15004
+ if (platform !== "darwin" && platform !== "linux" && platform !== "win32")
15005
+ return null;
15006
+ const arch = process.arch;
15007
+ if (arch === "x64")
15008
+ return { platform, arch: "x64" };
15009
+ if (arch === "arm64")
15010
+ return { platform, arch: "arm64" };
15011
+ return null;
15012
+ }
15013
+
15014
+ // src/lsp-github-install.ts
15015
+ function ghCacheRoot() {
15016
+ return join5(aftCacheBase(), "lsp-binaries");
15017
+ }
15018
+ function ghPackageDir(spec) {
15019
+ return join5(ghCacheRoot(), spec.id);
15020
+ }
15021
+ function ghBinDir(spec) {
15022
+ return join5(ghPackageDir(spec), "bin");
15023
+ }
15024
+ function ghExtractDir(spec) {
15025
+ return join5(ghPackageDir(spec), "extracted");
15026
+ }
15027
+ function ghBinaryPath(spec, platform) {
15028
+ const ext = platform === "win32" ? ".exe" : "";
15029
+ return join5(ghBinDir(spec), `${spec.binary}${ext}`);
15030
+ }
15031
+ function isGithubInstalled(spec, platform) {
15032
+ for (const candidate of ghBinaryCandidates(spec, platform)) {
15033
+ try {
15034
+ if (statSync3(join5(ghBinDir(spec), candidate)).isFile())
15035
+ return true;
15036
+ } catch {}
15037
+ }
15038
+ return false;
15039
+ }
15040
+ function ghBinaryCandidates(spec, platform) {
15041
+ if (platform !== "win32")
15042
+ return [spec.binary];
15043
+ return [spec.binary, `${spec.binary}.cmd`, `${spec.binary}.exe`, `${spec.binary}.bat`];
15044
+ }
15045
+ var MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
15046
+ var MAX_EXTRACT_BYTES = 1024 * 1024 * 1024;
15047
+ function sha256OfFile(path2) {
15048
+ return new Promise((resolve2, reject) => {
15049
+ const hash2 = createHash2("sha256");
15050
+ const stream = createReadStream2(path2);
15051
+ stream.on("error", reject);
15052
+ stream.on("data", (chunk) => hash2.update(chunk));
15053
+ stream.on("end", () => resolve2(hash2.digest("hex")));
15054
+ });
15055
+ }
15056
+ async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
15057
+ const candidates = [];
15058
+ candidates.push(tag);
15059
+ if (!tag.startsWith("v")) {
15060
+ candidates.push(`v${tag}`);
15061
+ } else {
15062
+ candidates.push(tag.slice(1));
15063
+ }
15064
+ const headers = {
15065
+ accept: "application/vnd.github+json",
15066
+ "user-agent": "aft-opencode",
15067
+ "x-github-api-version": "2022-11-28"
15068
+ };
15069
+ if (process.env.GITHUB_TOKEN) {
15070
+ headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
15071
+ }
15072
+ for (const candidate of candidates) {
15073
+ const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
15074
+ const timeout = controlledTimeoutSignal(15000, signal);
15075
+ try {
15076
+ const res = await fetchImpl(url2, {
15077
+ headers,
15078
+ redirect: "follow",
15079
+ signal: timeout.signal
15080
+ });
15081
+ if (res.status === 404)
15082
+ continue;
15083
+ if (!res.ok) {
15084
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: HTTP ${res.status}`);
15085
+ return null;
15086
+ }
15087
+ const json2 = await res.json();
15088
+ if (!json2.tag_name || !Array.isArray(json2.assets)) {
15089
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: malformed response`);
15090
+ return null;
15091
+ }
15092
+ const assets = json2.assets.filter((a) => typeof a.name === "string" && typeof a.browser_download_url === "string").map((a) => ({
15093
+ name: a.name,
15094
+ url: a.browser_download_url,
15095
+ size: typeof a.size === "number" ? a.size : undefined
15096
+ }));
15097
+ return { tag: json2.tag_name, assets };
15098
+ } catch (err) {
15099
+ if (signal?.aborted) {
15100
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: aborted`);
15101
+ return null;
15102
+ }
15103
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: ${err}`);
15104
+ } finally {
15105
+ timeout.cleanup();
15106
+ }
15107
+ }
15108
+ return null;
15109
+ }
15110
+ async function resolveTargetTag(spec, config2, fetchImpl, signal) {
15111
+ const pinned = config2.versions[spec.githubRepo];
15112
+ if (pinned) {
15113
+ try {
15114
+ assertSafeVersion(pinned);
15115
+ } catch (err) {
15116
+ return {
15117
+ tag: null,
15118
+ assets: [],
15119
+ blockedByGrace: false,
15120
+ reason: `invalid pinned version ${JSON.stringify(pinned)}: ${err instanceof Error ? err.message : String(err)}`
15121
+ };
15122
+ }
15123
+ const release = await fetchReleaseByTag(spec.githubRepo, pinned, fetchImpl, signal);
15124
+ if (release) {
15125
+ return {
15126
+ tag: release.tag,
15127
+ assets: release.assets,
15128
+ blockedByGrace: false
15129
+ };
15130
+ }
15131
+ return {
15132
+ tag: null,
15133
+ assets: [],
15134
+ blockedByGrace: false,
15135
+ reason: `pinned tag ${pinned} not found on GitHub`
15136
+ };
15137
+ }
15138
+ const cached2 = readVersionCheck(spec.githubRepo);
15139
+ const weeklyMs = config2.graceDays * 24 * 60 * 60 * 1000;
15140
+ const cachedTag = cached2?.latest_eligible ?? null;
15141
+ const cachedSafe = isSafeVersion(cachedTag);
15142
+ if (cached2 && !shouldRecheckVersion(cached2, weeklyMs) && cachedSafe) {
15143
+ const release = await fetchReleaseByTag(spec.githubRepo, cachedTag, fetchImpl);
15144
+ if (release) {
15145
+ return {
15146
+ tag: release.tag,
15147
+ assets: release.assets,
15148
+ blockedByGrace: false
15149
+ };
15150
+ }
15151
+ }
15152
+ const probe = await probeGithubReleases(spec.githubRepo, config2.graceDays, fetchImpl);
15153
+ if (!probe) {
15154
+ return {
15155
+ tag: null,
15156
+ assets: [],
15157
+ blockedByGrace: false,
15158
+ reason: "github releases probe failed"
15159
+ };
15160
+ }
15161
+ writeVersionCheck(spec.githubRepo, probe.tag);
15162
+ return { tag: probe.tag, assets: probe.assets, blockedByGrace: probe.blockedByGrace };
15163
+ }
15164
+ function controlledTimeoutSignal(timeoutMs, parent) {
15165
+ const controller = new AbortController;
15166
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
15167
+ timeout.unref?.();
15168
+ const abort = () => controller.abort();
15169
+ parent?.addEventListener("abort", abort, { once: true });
15170
+ if (parent?.aborted)
15171
+ abort();
15172
+ return {
15173
+ signal: controller.signal,
15174
+ cleanup: () => {
15175
+ clearTimeout(timeout);
15176
+ parent?.removeEventListener("abort", abort);
15177
+ }
15178
+ };
15179
+ }
15180
+ var ALLOWED_DOWNLOAD_HOSTS = new Set([
15181
+ "github.com",
15182
+ "api.github.com",
15183
+ "objects.githubusercontent.com",
15184
+ "release-assets.githubusercontent.com",
15185
+ "raw.githubusercontent.com",
15186
+ "codeload.github.com"
15187
+ ]);
15188
+ function assertAllowedDownloadUrl(rawUrl) {
15189
+ let parsed;
15190
+ try {
15191
+ parsed = new URL(rawUrl);
15192
+ } catch {
15193
+ throw new Error(`download url is not a valid URL: ${rawUrl}`);
15194
+ }
15195
+ if (parsed.protocol !== "https:") {
15196
+ throw new Error(`download url must be https (got ${parsed.protocol}): ${rawUrl}`);
15197
+ }
15198
+ if (!ALLOWED_DOWNLOAD_HOSTS.has(parsed.hostname.toLowerCase())) {
15199
+ throw new Error(`download url host ${parsed.hostname} is not in the GitHub allowlist: ${rawUrl}`);
15200
+ }
15201
+ return parsed;
15202
+ }
15203
+ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
15204
+ assertAllowedDownloadUrl(url2);
15205
+ if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES) {
15206
+ throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES} (set lsp.versions to pin a smaller release if this is wrong)`);
15207
+ }
15208
+ const timeout = controlledTimeoutSignal(120000, signal);
15209
+ try {
15210
+ const res = await fetchImpl(url2, {
15211
+ headers: { accept: "application/octet-stream" },
15212
+ redirect: "follow",
15213
+ signal: timeout.signal
15214
+ });
15215
+ if (!res.ok || !res.body) {
15216
+ throw new Error(`download failed (${res.status})`);
15217
+ }
15218
+ const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
15219
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES) {
15220
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES}`);
15221
+ }
15222
+ mkdirSync2(dirname(destPath), { recursive: true });
15223
+ let bytesWritten = 0;
15224
+ const guard = new TransformStream({
15225
+ transform(chunk, controller) {
15226
+ bytesWritten += chunk.byteLength;
15227
+ if (bytesWritten > MAX_DOWNLOAD_BYTES) {
15228
+ controller.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES} bytes after streaming (server lied about size or sent unbounded body)`));
15229
+ return;
15230
+ }
15231
+ controller.enqueue(chunk);
15232
+ }
15233
+ });
15234
+ const guarded = res.body.pipeThrough(guard);
15235
+ const nodeStream = Readable.fromWeb(guarded);
15236
+ await pipeline(nodeStream, createWriteStream(destPath), { signal: timeout.signal });
15237
+ } catch (err) {
15238
+ try {
15239
+ unlinkSync2(destPath);
15240
+ } catch {}
15241
+ throw err;
15242
+ } finally {
15243
+ timeout.cleanup();
15244
+ }
15245
+ }
15246
+ function validateExtraction(stagingRoot) {
15247
+ const realStagingRoot = realpathSync(stagingRoot);
15248
+ let totalBytes = 0;
15249
+ const walk = (dir) => {
15250
+ let entries;
15251
+ try {
15252
+ entries = readdirSync2(dir);
15253
+ } catch (err) {
15254
+ throw new Error(`failed to read staging dir ${dir}: ${err}`);
15255
+ }
15256
+ for (const entry of entries) {
15257
+ const full = join5(dir, entry);
15258
+ let lst;
15259
+ try {
15260
+ lst = lstatSync(full);
15261
+ } catch (err) {
15262
+ throw new Error(`failed to lstat ${full}: ${err}`);
15263
+ }
15264
+ if (lst.isSymbolicLink()) {
15265
+ let target = "<unreadable>";
15266
+ try {
15267
+ target = readlinkSync(full);
15268
+ } catch {}
15269
+ throw new Error(`archive contains symlink ${relative(realStagingRoot, full)} → ${target}; rejecting (zip-slip defense)`);
15270
+ }
15271
+ let realFull;
15272
+ try {
15273
+ realFull = realpathSync(full);
15274
+ } catch (err) {
15275
+ throw new Error(`failed to realpath ${full}: ${err}`);
15276
+ }
15277
+ const rel = relative(realStagingRoot, realFull);
15278
+ if (rel.startsWith("..") || resolve(realStagingRoot, rel) !== realFull) {
15279
+ throw new Error(`archive entry escapes staging root: ${full} → ${realFull} (zip-slip defense)`);
15280
+ }
15281
+ if (lst.isDirectory()) {
15282
+ walk(full);
15283
+ } else if (lst.isFile()) {
15284
+ totalBytes += lst.size;
15285
+ if (totalBytes > MAX_EXTRACT_BYTES) {
15286
+ throw new Error(`extracted archive exceeds ${MAX_EXTRACT_BYTES} bytes (decompression bomb defense): saw ${totalBytes} bytes before hitting the cap`);
15287
+ }
15288
+ } else {
15289
+ throw new Error(`archive contains non-file/non-dir entry: ${full}`);
15290
+ }
15291
+ }
15292
+ };
15293
+ walk(realStagingRoot);
15294
+ }
15295
+ function extractArchiveSafely(archivePath, destDir, archiveType) {
15296
+ const suffix = randomBytes(8).toString("hex");
15297
+ const stagingDir = `${destDir}.staging-${suffix}`;
15298
+ try {
15299
+ rmSync(stagingDir, { recursive: true, force: true });
15300
+ } catch {}
15301
+ mkdirSync2(stagingDir, { recursive: true });
15302
+ try {
15303
+ runPlatformExtractor(archivePath, stagingDir, archiveType);
15304
+ validateExtraction(stagingDir);
15305
+ try {
15306
+ rmSync(destDir, { recursive: true, force: true });
15307
+ } catch {}
15308
+ renameSync(stagingDir, destDir);
15309
+ } catch (err) {
15310
+ try {
15311
+ rmSync(stagingDir, { recursive: true, force: true });
15312
+ } catch {}
15313
+ throw err;
15314
+ }
15315
+ }
15316
+ function runPlatformExtractor(archivePath, destDir, archiveType) {
15317
+ if (archiveType === "zip") {
15318
+ if (process.platform === "win32") {
15319
+ execFileSync("tar.exe", ["-xf", archivePath, "-C", destDir], {
15320
+ stdio: "pipe",
15321
+ timeout: 180000
15322
+ });
15323
+ return;
15324
+ }
15325
+ execFileSync("unzip", ["-q", "-o", archivePath, "-d", destDir], {
15326
+ stdio: "pipe",
15327
+ timeout: 180000
15328
+ });
15329
+ return;
15330
+ }
15331
+ if (archiveType === "tar.gz") {
15332
+ execFileSync("tar", ["-xzf", archivePath, "-C", destDir], {
15333
+ stdio: "pipe",
15334
+ timeout: 180000
15335
+ });
15336
+ return;
15337
+ }
15338
+ if (archiveType === "tar.xz") {
15339
+ execFileSync("tar", ["-xf", archivePath, "-C", destDir], {
15340
+ stdio: "pipe",
15341
+ timeout: 180000
15342
+ });
15343
+ return;
15344
+ }
15345
+ throw new Error(`unsupported archive type: ${archiveType}`);
15346
+ }
15347
+ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl, signal) {
15348
+ const version2 = stripTagV(tag);
15349
+ const expected = spec.resolveAsset(platform, arch, version2);
15350
+ if (!expected) {
15351
+ warn(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform}/${arch}`);
15352
+ return null;
15353
+ }
15354
+ const matchingAsset = assets.find((a) => a.name === expected.name);
15355
+ if (!matchingAsset) {
15356
+ warn(`[lsp] ${spec.id}: asset ${expected.name} not found in release ${tag} (${assets.length} assets available)`);
15357
+ return null;
15358
+ }
15359
+ const pkgDir = ghPackageDir(spec);
15360
+ const extractDir = ghExtractDir(spec);
15361
+ const archivePath = join5(pkgDir, expected.name);
15362
+ log(`[lsp] downloading ${spec.id} ${tag} → ${matchingAsset.url}`);
15363
+ try {
15364
+ await downloadFile(matchingAsset.url, archivePath, fetchImpl, matchingAsset.size, signal);
15365
+ } catch (err) {
15366
+ error48(`[lsp] download ${spec.id} failed: ${err}`);
15367
+ return null;
15368
+ }
15369
+ let archiveSha256;
15370
+ try {
15371
+ archiveSha256 = await sha256OfFile(archivePath);
15372
+ } catch (err) {
15373
+ error48(`[lsp] hash ${spec.id} failed: ${err}`);
15374
+ try {
15375
+ unlinkSync2(archivePath);
15376
+ } catch {}
15377
+ return null;
15378
+ }
15379
+ log(`[lsp] ${spec.id} ${tag} sha256=${archiveSha256}`);
15380
+ const previousMeta = readInstalledMetaIn(ghPackageDir(spec));
15381
+ if (previousMeta && previousMeta.version === tag && previousMeta.sha256) {
15382
+ if (previousMeta.sha256 !== archiveSha256) {
15383
+ error48(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch — refusing install. ` + `Previously installed sha256=${previousMeta.sha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding.`);
15384
+ try {
15385
+ unlinkSync2(archivePath);
15386
+ } catch {}
15387
+ return null;
15388
+ }
15389
+ }
15390
+ try {
15391
+ extractArchiveSafely(archivePath, extractDir, expected.archive);
15392
+ } catch (err) {
15393
+ error48(`[lsp] extract ${spec.id} failed: ${err}`);
15394
+ return null;
15395
+ } finally {
15396
+ try {
15397
+ unlinkSync2(archivePath);
15398
+ } catch {}
15399
+ }
15400
+ const innerBinaryPath = join5(extractDir, spec.binaryPathInArchive(platform, arch, version2));
15401
+ if (!existsSync3(innerBinaryPath)) {
15402
+ error48(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
15403
+ return null;
15404
+ }
15405
+ const targetBinary = ghBinaryPath(spec, platform);
15406
+ mkdirSync2(dirname(targetBinary), { recursive: true });
15407
+ try {
15408
+ copyFileSync(innerBinaryPath, targetBinary);
15409
+ if (platform !== "win32") {
15410
+ const { chmodSync } = await import("node:fs");
15411
+ chmodSync(targetBinary, 493);
15412
+ }
15413
+ } catch (err) {
15414
+ error48(`[lsp] ${spec.id}: failed to place binary at ${targetBinary}: ${err}`);
15415
+ return null;
15416
+ }
15417
+ log(`[lsp] installed ${spec.id} ${tag} at ${targetBinary}`);
15418
+ return archiveSha256;
15419
+ }
15420
+ async function ensureGithubInstalled(spec, config2, fetchImpl, platform, arch, signal) {
15421
+ const outcome = await withInstallLock(spec.githubRepo, async () => {
15422
+ const { tag, assets, blockedByGrace, reason } = await resolveTargetTag(spec, config2, fetchImpl, signal);
15423
+ if (!tag) {
15424
+ const installed = isGithubInstalled(spec, platform);
15425
+ if (installed) {
15426
+ warn(`[lsp] no eligible release of ${spec.githubRepo} (grace=${config2.graceDays}d); keeping existing install`);
15427
+ return { started: false, reason: "kept existing install" };
15428
+ }
15429
+ const fallbackReason = reason ?? (blockedByGrace ? `all releases are within ${config2.graceDays}-day grace window` : "github releases probe failed");
15430
+ warn(`[lsp] skipping ${spec.id}: ${fallbackReason}`);
15431
+ return { started: false, reason: fallbackReason };
15432
+ }
15433
+ if (isGithubInstalled(spec, platform)) {
15434
+ const installedMeta = readInstalledMetaIn(ghPackageDir(spec));
15435
+ if (installedMeta && installedMeta.version === tag) {
15436
+ return { started: false, reason: "already installed" };
15437
+ }
15438
+ if (installedMeta) {
15439
+ log(`[lsp] reinstalling ${spec.id}: cached ${installedMeta.version} ≠ target ${tag}`);
15440
+ } else {
15441
+ log(`[lsp] reinstalling ${spec.id}@${tag}: no installed-version metadata recorded`);
15442
+ }
15443
+ }
15444
+ const archiveSha256 = await downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl, signal).catch((err) => {
15445
+ error48(`[lsp] github install ${spec.id} crashed: ${err}`);
15446
+ return null;
15447
+ });
15448
+ if (!archiveSha256) {
15449
+ return { started: true, reason: "install failed (see plugin log)" };
15450
+ }
15451
+ writeInstalledMetaIn(ghPackageDir(spec), tag, archiveSha256);
15452
+ return { started: true };
15453
+ });
15454
+ if (outcome === null) {
15455
+ return { started: false, reason: "another install in progress" };
15456
+ }
15457
+ return outcome;
15458
+ }
15459
+ var inFlightGithubInstalls = new Set;
15460
+ function trackInFlightGithubInstall(controller, promise2) {
15461
+ const entry = { controller, promise: promise2 };
15462
+ inFlightGithubInstalls.add(entry);
15463
+ promise2.then(() => inFlightGithubInstalls.delete(entry), () => inFlightGithubInstalls.delete(entry));
15464
+ return promise2;
15465
+ }
15466
+ async function abortInFlightGithubInstalls() {
15467
+ const installs = Array.from(inFlightGithubInstalls);
15468
+ for (const install of installs) {
15469
+ install.controller.abort();
15470
+ }
15471
+ await Promise.allSettled(installs.map((install) => install.promise));
15472
+ }
15473
+ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
15474
+ const cachedBinDirs = [];
15475
+ const skipped = [];
15476
+ const installPromises = [];
15477
+ let installsStarted = 0;
15478
+ const host = detectHostPlatform();
15479
+ if (!host) {
15480
+ for (const spec of GITHUB_LSP_TABLE) {
15481
+ try {
15482
+ if (existsSync3(ghBinDir(spec))) {
15483
+ cachedBinDirs.push(ghBinDir(spec));
15484
+ }
15485
+ } catch {}
15486
+ }
15487
+ return {
15488
+ cachedBinDirs,
15489
+ installsStarted: 0,
15490
+ skipped,
15491
+ installsComplete: Promise.resolve()
15492
+ };
15493
+ }
15494
+ for (const spec of GITHUB_LSP_TABLE) {
15495
+ if (isGithubInstalled(spec, host.platform)) {
15496
+ cachedBinDirs.push(ghBinDir(spec));
15497
+ }
15498
+ if (config2.disabled.has(spec.id)) {
15499
+ skipped.push({ id: spec.id, reason: "disabled by config" });
15500
+ continue;
15501
+ }
15502
+ if (!config2.autoInstall) {
15503
+ skipped.push({ id: spec.id, reason: "auto_install: false" });
15504
+ continue;
15505
+ }
15506
+ if (!relevantServers.has(spec.id)) {
15507
+ skipped.push({ id: spec.id, reason: "not relevant to project" });
15508
+ continue;
15509
+ }
15510
+ installsStarted += 1;
15511
+ const controller = new AbortController;
15512
+ const promise2 = ensureGithubInstalled(spec, config2, fetchImpl, host.platform, host.arch, controller.signal).then((outcome) => {
15513
+ if (!outcome.started)
15514
+ installsStarted -= 1;
15515
+ if (outcome.reason && outcome.reason !== "already installed") {
15516
+ skipped.push({ id: spec.id, reason: outcome.reason });
15517
+ }
15518
+ }, (err) => {
15519
+ installsStarted -= 1;
15520
+ const reason = err instanceof Error ? err.message : String(err);
15521
+ skipped.push({ id: spec.id, reason: `install error: ${reason}` });
15522
+ error48(`[lsp] github install ${spec.id} promise rejected: ${reason}`);
15523
+ });
15524
+ installPromises.push(trackInFlightGithubInstall(controller, promise2));
15525
+ }
15526
+ return {
15527
+ cachedBinDirs,
15528
+ get installsStarted() {
15529
+ return installsStarted;
15530
+ },
15531
+ skipped,
15532
+ installsComplete: Promise.all(installPromises).then(() => {})
15533
+ };
15534
+ }
15535
+ function discoverRelevantGithubServers(projectRoot) {
15536
+ const extToServerIds = {
15537
+ c: ["clangd"],
15538
+ "c++": ["clangd"],
15539
+ cc: ["clangd"],
15540
+ cpp: ["clangd"],
15541
+ cxx: ["clangd"],
15542
+ h: ["clangd"],
15543
+ "h++": ["clangd"],
15544
+ hpp: ["clangd"],
15545
+ hh: ["clangd"],
15546
+ hxx: ["clangd"],
15547
+ lua: ["lua-ls"],
15548
+ zig: ["zls"],
15549
+ zon: ["zls"],
15550
+ typ: ["tinymist"],
15551
+ typc: ["tinymist"],
15552
+ tex: ["texlab"],
15553
+ bib: ["texlab"]
15554
+ };
15555
+ const rootMarkers = {
15556
+ clangd: ["compile_commands.json", "compile_flags.txt", ".clangd"],
15557
+ "lua-ls": [".luarc.json", ".luarc.jsonc", ".stylua.toml", "stylua.toml"],
15558
+ zls: ["build.zig"],
15559
+ tinymist: ["typst.toml"],
15560
+ texlab: [".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]
15561
+ };
15562
+ const relevant = new Set;
15563
+ for (const spec of GITHUB_LSP_TABLE) {
15564
+ if (hasRootMarker(projectRoot, rootMarkers[spec.id]))
15565
+ relevant.add(spec.id);
15566
+ }
15567
+ const extensions = relevantExtensionsInProject(projectRoot, extToServerIds);
15568
+ for (const ext of extensions) {
15569
+ for (const id of extToServerIds[ext] ?? []) {
15570
+ relevant.add(id);
15571
+ }
15572
+ }
15573
+ return relevant;
15574
+ }
15575
+
15576
+ // src/notifications.ts
15577
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
15578
+ import { join as join6 } from "node:path";
15579
+ var WARNING_MARKER = "\uD83D\uDD27 AFT: ⚠️";
15580
+ var WARNED_TOOLS_FILE = "warned_tools.json";
15581
+ function sendIgnoredMessage(client, _sessionId, text) {
15582
+ const typedClient = client;
15583
+ if (typeof typedClient.ui?.notify !== "function")
15584
+ return false;
15585
+ try {
15586
+ typedClient.ui.notify(text, "warning");
15587
+ return true;
15588
+ } catch (err) {
15589
+ log(`[aft-pi] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
15590
+ return false;
15591
+ }
15592
+ }
15593
+ function readWarnedTools(storageDir) {
15594
+ try {
15595
+ const warnedToolsPath = join6(storageDir, WARNED_TOOLS_FILE);
15596
+ if (!existsSync4(warnedToolsPath))
15597
+ return {};
15598
+ const parsed = JSON.parse(readFileSync3(warnedToolsPath, "utf-8"));
15599
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
15600
+ return {};
15601
+ const warned = {};
15602
+ for (const [key, version2] of Object.entries(parsed)) {
15603
+ if (typeof version2 === "string") {
15604
+ warned[key] = version2;
15605
+ }
15606
+ }
15607
+ return warned;
15608
+ } catch {
15609
+ return {};
15610
+ }
15611
+ }
15612
+ function writeWarnedTools(storageDir, warned) {
15613
+ try {
15614
+ mkdirSync3(storageDir, { recursive: true });
15615
+ const warnedToolsPath = join6(storageDir, WARNED_TOOLS_FILE);
15616
+ writeFileSync2(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
15617
+ `);
15618
+ } catch {}
15619
+ }
15620
+ function warningKey(warning, projectRoot) {
15621
+ return [
15622
+ projectRoot ?? "_",
15623
+ warning.kind,
15624
+ warning.language ?? warning.server ?? "_",
15625
+ warning.tool ?? warning.binary ?? "_",
15626
+ warning.hint
15627
+ ].map((part) => encodeURIComponent(part)).join(":");
15628
+ }
15629
+ function warningTitle(warning) {
15630
+ switch (warning.kind) {
15631
+ case "formatter_not_installed":
15632
+ return "Formatter is not installed";
15633
+ case "checker_not_installed":
15634
+ return "Checker is not installed";
15635
+ case "lsp_binary_missing":
15636
+ return "LSP binary is missing";
15637
+ }
15638
+ }
15639
+ function formatConfigureWarning(warning) {
15640
+ const details = [];
15641
+ if (warning.language)
15642
+ details.push(`language: ${warning.language}`);
15643
+ if (warning.server)
15644
+ details.push(`server: ${warning.server}`);
15645
+ if (warning.tool)
15646
+ details.push(`tool: ${warning.tool}`);
15647
+ if (warning.binary && warning.binary !== warning.tool) {
15648
+ details.push(`binary: ${warning.binary}`);
15649
+ }
15650
+ const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
15651
+ return `${WARNING_MARKER} ${warningTitle(warning)}${suffix}
15652
+ ${warning.hint}`;
15653
+ }
15654
+ async function deliverConfigureWarnings(opts, warnings) {
15655
+ if (warnings.length === 0)
15656
+ return;
15657
+ const warned = readWarnedTools(opts.storageDir);
15658
+ let changed = false;
15659
+ for (const warning of warnings) {
15660
+ const key = warningKey(warning, opts.projectRoot);
15661
+ if (Object.hasOwn(warned, key))
15662
+ continue;
15663
+ if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning))) {
15664
+ continue;
15665
+ }
15666
+ warned[key] = opts.pluginVersion;
15667
+ changed = true;
15668
+ }
15669
+ if (changed) {
15670
+ writeWarnedTools(opts.storageDir, warned);
15671
+ }
15672
+ }
15673
+
15674
+ // src/onnx-runtime.ts
15675
+ import { execFileSync as execFileSync2 } from "node:child_process";
15676
+ import { createHash as createHash3 } from "node:crypto";
15677
+ import {
15678
+ chmodSync,
15679
+ closeSync as closeSync2,
15680
+ copyFileSync as copyFileSync2,
15681
+ createWriteStream as createWriteStream2,
15682
+ existsSync as existsSync5,
15683
+ lstatSync as lstatSync2,
15684
+ mkdirSync as mkdirSync4,
15685
+ openSync as openSync2,
15686
+ readdirSync as readdirSync3,
15687
+ readFileSync as readFileSync4,
15688
+ readlinkSync as readlinkSync2,
15689
+ realpathSync as realpathSync2,
15690
+ rmSync as rmSync2,
15691
+ statSync as statSync4,
15692
+ symlinkSync,
15693
+ unlinkSync as unlinkSync3,
15694
+ writeFileSync as writeFileSync3
15695
+ } from "node:fs";
15696
+ import { dirname as dirname2, join as join7, relative as relative2, resolve as resolve2 } from "node:path";
15697
+ import { Readable as Readable2 } from "node:stream";
15698
+ import { pipeline as pipeline2 } from "node:stream/promises";
15699
+ var ORT_VERSION = "1.24.4";
15700
+ var ORT_REPO = "microsoft/onnxruntime";
15701
+ var MAX_DOWNLOAD_BYTES2 = 256 * 1024 * 1024;
15702
+ var MAX_EXTRACT_BYTES2 = 1 * 1024 * 1024 * 1024;
15703
+ var ONNX_LOCK_FILE = ".aft-onnx-installing";
15704
+ var ONNX_INSTALLED_META_FILE = ".aft-onnx-installed";
15705
+ var STALE_LOCK_MS2 = 30 * 60 * 1000;
15706
+ var ORT_PLATFORM_MAP = {
15707
+ darwin: {
15708
+ arm64: {
15709
+ assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
15710
+ libName: "libonnxruntime.dylib",
15711
+ archiveType: "tgz"
15712
+ }
15713
+ },
15714
+ linux: {
15715
+ x64: {
15716
+ assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
15717
+ libName: "libonnxruntime.so",
15718
+ archiveType: "tgz"
15719
+ },
15720
+ arm64: {
15721
+ assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
15722
+ libName: "libonnxruntime.so",
15723
+ archiveType: "tgz"
15724
+ }
15725
+ },
15726
+ win32: {
15727
+ x64: {
15728
+ assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
15729
+ libName: "onnxruntime.dll",
15730
+ archiveType: "zip"
15731
+ },
15732
+ arm64: {
15733
+ assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
15734
+ libName: "onnxruntime.dll",
15735
+ archiveType: "zip"
15736
+ }
15737
+ }
15738
+ };
15739
+ function getPlatformInfo() {
15740
+ const platformMap = ORT_PLATFORM_MAP[process.platform];
15741
+ if (!platformMap)
15742
+ return null;
15743
+ return platformMap[process.arch] || null;
15744
+ }
15745
+ function getManualInstallHint() {
15746
+ if (process.platform === "darwin" && process.arch === "x64") {
15747
+ return "brew install onnxruntime";
15748
+ }
15749
+ if (process.platform === "linux") {
15750
+ return "apt install libonnxruntime or download from https://github.com/microsoft/onnxruntime/releases";
15751
+ }
15752
+ return "Download from https://github.com/microsoft/onnxruntime/releases";
15753
+ }
15754
+ async function ensureOnnxRuntime(storageDir) {
15755
+ const info = getPlatformInfo();
15756
+ const ortDir = join7(storageDir, "onnxruntime", ORT_VERSION);
15757
+ const libPath = join7(ortDir, info?.libName ?? "libonnxruntime.dylib");
15758
+ if (existsSync5(libPath)) {
15759
+ const meta3 = readOnnxInstalledMeta(ortDir);
15760
+ if (meta3?.sha256) {
15761
+ try {
15762
+ const currentHash = sha256File(libPath);
15763
+ if (currentHash !== meta3.sha256) {
15764
+ error48(`ONNX Runtime at ${ortDir}: TOFU sha256 mismatch — refusing to use ` + `tampered binary. Recorded ${meta3.sha256}, current ${currentHash}. ` + `Run \`aft doctor --clear\` to re-download from scratch.`);
15765
+ } else {
15766
+ log(`ONNX Runtime found at ${ortDir} (TOFU verified)`);
15767
+ return ortDir;
15768
+ }
15769
+ } catch (err) {
15770
+ warn(`Could not verify ONNX Runtime hash at ${ortDir}: ${err}`);
15771
+ return ortDir;
15772
+ }
15773
+ } else {
15774
+ log(`ONNX Runtime found at ${ortDir} (no recorded hash, accepting)`);
15775
+ return ortDir;
15776
+ }
15777
+ }
15778
+ const systemPath = findSystemOnnxRuntime(info?.libName);
15779
+ if (systemPath) {
15780
+ log(`ONNX Runtime found at system path: ${systemPath}`);
15781
+ return systemPath;
15782
+ }
15783
+ if (!info) {
15784
+ warn(`ONNX Runtime auto-download not available for ${process.platform}/${process.arch}. Install manually: ${getManualInstallHint()}`);
15785
+ return null;
15786
+ }
15787
+ const onnxBaseDir = join7(storageDir, "onnxruntime");
15788
+ mkdirSync4(onnxBaseDir, { recursive: true });
15789
+ const lockPath2 = join7(onnxBaseDir, ONNX_LOCK_FILE);
15790
+ if (!acquireLock(lockPath2)) {
15791
+ warn(`ONNX Runtime install already in progress in another process (lock: ${lockPath2}). Skipping.`);
15792
+ return null;
15793
+ }
15794
+ try {
15795
+ return await downloadOnnxRuntime(info, ortDir);
15796
+ } finally {
15797
+ releaseLock(lockPath2);
15798
+ }
15799
+ }
15800
+ function findSystemOnnxRuntime(libName) {
15801
+ if (!libName)
15802
+ return null;
15803
+ const searchPaths = [];
15804
+ if (process.platform === "darwin") {
15805
+ searchPaths.push("/opt/homebrew/lib", "/usr/local/lib");
15806
+ } else if (process.platform === "linux") {
15807
+ searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
15808
+ }
15809
+ for (const dir of searchPaths) {
15810
+ if (existsSync5(join7(dir, libName))) {
15811
+ return dir;
15812
+ }
15813
+ }
15814
+ return null;
15815
+ }
15816
+ async function downloadFileWithCap(url2, destPath) {
15817
+ const controller = new AbortController;
15818
+ const timeout = setTimeout(() => controller.abort(), 300000);
15819
+ try {
15820
+ const res = await fetch(url2, {
15821
+ headers: { accept: "application/octet-stream" },
15822
+ redirect: "follow",
15823
+ signal: controller.signal
15824
+ });
15825
+ if (!res.ok || !res.body) {
15826
+ throw new Error(`download failed (HTTP ${res.status})`);
15827
+ }
15828
+ const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
15829
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
15830
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
15831
+ }
15832
+ mkdirSync4(dirname2(destPath), { recursive: true });
15833
+ let bytesWritten = 0;
15834
+ const guard = new TransformStream({
15835
+ transform(chunk, transformController) {
15836
+ bytesWritten += chunk.byteLength;
15837
+ if (bytesWritten > MAX_DOWNLOAD_BYTES2) {
15838
+ transformController.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES2} bytes after streaming (server lied about size or sent unbounded body)`));
15839
+ return;
15840
+ }
15841
+ transformController.enqueue(chunk);
15842
+ }
15843
+ });
15844
+ const guarded = res.body.pipeThrough(guard);
15845
+ const nodeStream = Readable2.fromWeb(guarded);
15846
+ await pipeline2(nodeStream, createWriteStream2(destPath), { signal: controller.signal });
15847
+ } catch (err) {
15848
+ try {
15849
+ unlinkSync3(destPath);
15850
+ } catch {}
15851
+ throw err;
15852
+ } finally {
15853
+ clearTimeout(timeout);
15854
+ }
15855
+ }
15856
+ function validateExtractedTree(stagingRoot) {
15857
+ const realRoot = realpathSync2(stagingRoot);
15858
+ let totalBytes = 0;
15859
+ const walk = (dir) => {
15860
+ const entries = readdirSync3(dir);
15861
+ for (const entry of entries) {
15862
+ const fullPath = join7(dir, entry);
15863
+ const lst = lstatSync2(fullPath);
15864
+ if (lst.isSymbolicLink()) {
15865
+ const linkTarget = readlinkSync2(fullPath);
15866
+ const resolvedTarget = resolve2(dirname2(fullPath), linkTarget);
15867
+ const rel2 = relative2(realRoot, resolvedTarget);
15868
+ if (rel2.startsWith("..") || process.platform !== "win32" && rel2.startsWith("/")) {
15869
+ throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
15870
+ }
15871
+ continue;
15872
+ }
15873
+ const rel = relative2(realRoot, fullPath);
15874
+ if (rel.startsWith("..") || process.platform !== "win32" && rel.startsWith("/")) {
15875
+ throw new Error(`extracted entry ${fullPath} escapes staging root`);
15876
+ }
15877
+ if (lst.isDirectory()) {
15878
+ walk(fullPath);
15879
+ continue;
15880
+ }
15881
+ if (lst.isFile()) {
15882
+ totalBytes += lst.size;
15883
+ if (totalBytes > MAX_EXTRACT_BYTES2) {
15884
+ throw new Error(`extracted size ${totalBytes} exceeds max ${MAX_EXTRACT_BYTES2} (decompression bomb defense)`);
15885
+ }
15886
+ }
15887
+ }
15888
+ };
15889
+ walk(realRoot);
15890
+ }
15891
+ async function downloadOnnxRuntime(info, targetDir) {
15892
+ const url2 = `https://github.com/${ORT_REPO}/releases/download/v${ORT_VERSION}/${info.assetName}.${info.archiveType === "tgz" ? "tgz" : "zip"}`;
15893
+ log(`Downloading ONNX Runtime v${ORT_VERSION} for ${process.platform}/${process.arch}...`);
15894
+ const tmpDir = `${targetDir}.tmp.${process.pid}.${Date.now().toString(36)}`;
15895
+ try {
15896
+ mkdirSync4(tmpDir, { recursive: true });
15897
+ const archivePath = join7(tmpDir, `onnxruntime.${info.archiveType}`);
15898
+ await downloadFileWithCap(url2, archivePath);
15899
+ const archiveSha256 = sha256File(archivePath);
15900
+ log(`ONNX Runtime archive sha256=${archiveSha256}`);
15901
+ if (info.archiveType === "tgz") {
15902
+ execFileSync2("tar", ["xzf", archivePath, "-C", tmpDir], {
15903
+ stdio: "pipe",
15904
+ timeout: 120000
15905
+ });
15906
+ } else {
15907
+ await extractZipArchive(archivePath, tmpDir);
15908
+ }
15909
+ try {
15910
+ unlinkSync3(archivePath);
15911
+ } catch {}
15912
+ validateExtractedTree(tmpDir);
15913
+ const extractedDir = join7(tmpDir, info.assetName, "lib");
15914
+ if (!existsSync5(extractedDir)) {
15915
+ throw new Error(`Expected directory not found: ${extractedDir}`);
15916
+ }
15917
+ mkdirSync4(targetDir, { recursive: true });
15918
+ const libFiles = readdirSync3(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
15919
+ const realFiles = [];
15920
+ const symlinks = [];
15921
+ for (const libFile of libFiles) {
15922
+ const src = join7(extractedDir, libFile);
15923
+ try {
15924
+ const stat = lstatSync2(src);
15925
+ log(`ORT extract: ${libFile} — isSymlink=${stat.isSymbolicLink()}, isFile=${stat.isFile()}, size=${stat.size}`);
15926
+ if (stat.isSymbolicLink()) {
15927
+ symlinks.push({ name: libFile, target: readlinkSync2(src) });
15928
+ } else {
15929
+ realFiles.push(libFile);
15930
+ }
15931
+ } catch (e) {
15932
+ log(`ORT extract: ${libFile} — stat failed: ${e}`);
15933
+ realFiles.push(libFile);
15934
+ }
15935
+ }
15936
+ for (const libFile of realFiles) {
15937
+ const src = join7(extractedDir, libFile);
15938
+ const dst = join7(targetDir, libFile);
14338
15939
  try {
14339
- cpFile(src, dst);
15940
+ copyFileSync2(src, dst);
14340
15941
  if (process.platform !== "win32") {
14341
15942
  chmodSync(dst, 493);
14342
15943
  }
@@ -14345,65 +15946,182 @@ async function downloadOnnxRuntime(info, targetDir) {
14345
15946
  }
14346
15947
  }
14347
15948
  for (const link of symlinks) {
14348
- const dst = join4(targetDir, link.name);
15949
+ const dst = join7(targetDir, link.name);
14349
15950
  try {
14350
- unlinkSync(dst);
15951
+ unlinkSync3(dst);
14351
15952
  } catch {}
14352
15953
  symlinkSync(link.target, dst);
14353
15954
  }
14354
- const { rmSync } = await import("node:fs");
14355
- rmSync(tmpDir, { recursive: true, force: true });
15955
+ const libPath = join7(targetDir, info.libName);
15956
+ let libHash = null;
15957
+ try {
15958
+ libHash = sha256File(libPath);
15959
+ } catch (err) {
15960
+ warn(`Could not hash newly-installed ONNX library at ${libPath}: ${err}`);
15961
+ }
15962
+ writeOnnxInstalledMeta(targetDir, ORT_VERSION, libHash, archiveSha256);
15963
+ rmSync2(tmpDir, { recursive: true, force: true });
14356
15964
  log(`ONNX Runtime v${ORT_VERSION} installed to ${targetDir}`);
14357
15965
  return targetDir;
14358
15966
  } catch (err) {
14359
15967
  error48(`Failed to download ONNX Runtime: ${err}`);
14360
15968
  try {
14361
- const { rmSync } = await import("node:fs");
14362
- rmSync(`${targetDir}.tmp.${process.pid}`, { recursive: true, force: true });
15969
+ rmSync2(tmpDir, { recursive: true, force: true });
15970
+ } catch {}
15971
+ try {
15972
+ rmSync2(targetDir, { recursive: true, force: true });
14363
15973
  } catch {}
14364
15974
  return null;
14365
15975
  }
14366
15976
  }
14367
15977
  async function extractZipArchive(archivePath, destinationDir) {
14368
- const { execFileSync } = await import("node:child_process");
14369
15978
  if (process.platform === "win32") {
14370
- let powershellError;
15979
+ execFileSync2("tar.exe", ["-xf", archivePath, "-C", destinationDir], {
15980
+ stdio: "pipe",
15981
+ timeout: 120000
15982
+ });
15983
+ return;
15984
+ }
15985
+ execFileSync2("unzip", ["-q", archivePath, "-d", destinationDir], {
15986
+ stdio: "pipe",
15987
+ timeout: 120000
15988
+ });
15989
+ }
15990
+ function writeOnnxInstalledMeta(installDir, version2, sha256, archiveSha256) {
15991
+ try {
15992
+ const meta3 = {
15993
+ version: version2,
15994
+ installedAt: new Date().toISOString(),
15995
+ ...sha256 ? { sha256 } : {},
15996
+ archiveSha256
15997
+ };
15998
+ writeFileSync3(join7(installDir, ONNX_INSTALLED_META_FILE), JSON.stringify(meta3), "utf8");
15999
+ } catch (err) {
16000
+ log(`[onnx] failed to write installed-meta in ${installDir}: ${err}`);
16001
+ }
16002
+ }
16003
+ function readOnnxInstalledMeta(installDir) {
16004
+ const path2 = join7(installDir, ONNX_INSTALLED_META_FILE);
16005
+ try {
16006
+ if (!statSync4(path2).isFile())
16007
+ return null;
16008
+ const raw = readFileSync4(path2, "utf8");
16009
+ const parsed = JSON.parse(raw);
16010
+ if (typeof parsed.version !== "string" || parsed.version.length === 0)
16011
+ return null;
16012
+ return {
16013
+ version: parsed.version,
16014
+ installedAt: typeof parsed.installedAt === "string" ? parsed.installedAt : "",
16015
+ ...typeof parsed.sha256 === "string" && parsed.sha256.length > 0 ? { sha256: parsed.sha256 } : {},
16016
+ ...typeof parsed.archiveSha256 === "string" && parsed.archiveSha256.length > 0 ? { archiveSha256: parsed.archiveSha256 } : {}
16017
+ };
16018
+ } catch {
16019
+ return null;
16020
+ }
16021
+ }
16022
+ function sha256File(path2) {
16023
+ const hash2 = createHash3("sha256");
16024
+ hash2.update(readFileSync4(path2));
16025
+ return hash2.digest("hex");
16026
+ }
16027
+ function acquireLock(lockPath2) {
16028
+ const tryClaim = () => {
14371
16029
  try {
14372
- execFileSync("powershell.exe", [
14373
- "-NoProfile",
14374
- "-NonInteractive",
14375
- "-ExecutionPolicy",
14376
- "Bypass",
14377
- "-Command",
14378
- "& { Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force }",
14379
- archivePath,
14380
- destinationDir
14381
- ], { stdio: "pipe", timeout: 120000 });
14382
- return;
16030
+ const fd = openSync2(lockPath2, "wx");
16031
+ try {
16032
+ writeFileSync3(fd, `${process.pid}
16033
+ ${new Date().toISOString()}
16034
+ `);
16035
+ } finally {
16036
+ closeSync2(fd);
16037
+ }
16038
+ return true;
14383
16039
  } catch (err) {
14384
- powershellError = err;
14385
- warn(`PowerShell Expand-Archive failed, falling back to cmd/tar: ${String(err)}`);
16040
+ const code = err.code;
16041
+ if (code === "EEXIST")
16042
+ return false;
16043
+ warn(`[onnx] unexpected error acquiring lock ${lockPath2}: ${err}`);
16044
+ return false;
14386
16045
  }
16046
+ };
16047
+ if (tryClaim())
16048
+ return true;
16049
+ let owningPid = null;
16050
+ let lockMtimeMs = 0;
16051
+ try {
16052
+ const raw = readFileSync4(lockPath2, "utf8");
16053
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
16054
+ const parsed = Number.parseInt(firstLine, 10);
16055
+ if (Number.isFinite(parsed) && parsed > 0)
16056
+ owningPid = parsed;
16057
+ lockMtimeMs = statSync4(lockPath2).mtimeMs;
16058
+ } catch {
16059
+ return tryClaim();
16060
+ }
16061
+ const age = Date.now() - lockMtimeMs;
16062
+ const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS2;
16063
+ const skipLiveness = process.platform === "win32";
16064
+ const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive2(owningPid);
16065
+ if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
16066
+ return false;
16067
+ }
16068
+ log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
16069
+ try {
16070
+ unlinkSync3(lockPath2);
16071
+ } catch {}
16072
+ return tryClaim();
16073
+ }
16074
+ function releaseLock(lockPath2) {
16075
+ try {
16076
+ let owningPid = null;
14387
16077
  try {
14388
- execFileSync("cmd.exe", ["/d", "/s", "/c", `tar -xf "${archivePath}" -C "${destinationDir}"`], { stdio: "pipe", timeout: 120000 });
16078
+ const raw = readFileSync4(lockPath2, "utf8");
16079
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
16080
+ const parsed = Number.parseInt(firstLine, 10);
16081
+ if (Number.isFinite(parsed) && parsed > 0)
16082
+ owningPid = parsed;
16083
+ } catch (readErr) {
16084
+ const code = readErr.code;
16085
+ if (code === "ENOENT")
16086
+ return;
16087
+ warn(`[onnx] could not read lock ${lockPath2} during release: ${readErr}`);
16088
+ return;
16089
+ }
16090
+ if (owningPid !== process.pid) {
16091
+ log(`[onnx] not releasing lock ${lockPath2}: owned by pid ${owningPid ?? "unknown"} (we are ${process.pid})`);
14389
16092
  return;
14390
- } catch (cmdError) {
14391
- throw new Error(`ZIP extraction failed via PowerShell and cmd/tar. PowerShell: ${String(powershellError)} | cmd/tar: ${String(cmdError)}`);
14392
16093
  }
16094
+ try {
16095
+ unlinkSync3(lockPath2);
16096
+ } catch (unlinkErr) {
16097
+ const code = unlinkErr.code;
16098
+ if (code !== "ENOENT") {
16099
+ warn(`[onnx] failed to release lock ${lockPath2}: ${unlinkErr}`);
16100
+ }
16101
+ }
16102
+ } catch (err) {
16103
+ warn(`[onnx] unexpected error releasing lock ${lockPath2}: ${err}`);
16104
+ }
16105
+ }
16106
+ function isProcessAlive2(pid) {
16107
+ try {
16108
+ process.kill(pid, 0);
16109
+ return true;
16110
+ } catch (err) {
16111
+ const code = err.code;
16112
+ if (code === "ESRCH")
16113
+ return false;
16114
+ return true;
14393
16115
  }
14394
- execFileSync("unzip", ["-q", archivePath, "-d", destinationDir], {
14395
- stdio: "pipe",
14396
- timeout: 120000
14397
- });
14398
16116
  }
14399
16117
 
14400
16118
  // src/pool.ts
14401
- import { realpathSync } from "node:fs";
16119
+ import { realpathSync as realpathSync3 } from "node:fs";
14402
16120
 
14403
16121
  // src/bridge.ts
14404
- import { spawn } from "node:child_process";
14405
- import { homedir as homedir2 } from "node:os";
14406
- import { join as join5 } from "node:path";
16122
+ import { spawn as spawn2 } from "node:child_process";
16123
+ import { homedir as homedir3 } from "node:os";
16124
+ import { join as join8 } from "node:path";
14407
16125
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
14408
16126
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
14409
16127
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
@@ -14545,14 +16263,14 @@ class BinaryBridge {
14545
16263
  const line = `${JSON.stringify(request)}
14546
16264
  `;
14547
16265
  const effectiveTimeoutMs = options?.timeoutMs ?? this.timeoutMs;
14548
- return new Promise((resolve, reject) => {
16266
+ return new Promise((resolve3, reject) => {
14549
16267
  const timer = setTimeout(() => {
14550
16268
  this.pending.delete(id);
14551
16269
  warn(`Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms — restarting bridge`);
14552
16270
  reject(new Error(`[aft-pi] Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
14553
16271
  this.handleTimeout();
14554
16272
  }, effectiveTimeoutMs);
14555
- this.pending.set(id, { resolve, reject, timer });
16273
+ this.pending.set(id, { resolve: resolve3, reject, timer });
14556
16274
  if (!this.process?.stdin?.writable) {
14557
16275
  this.pending.delete(id);
14558
16276
  clearTimeout(timer);
@@ -14595,15 +16313,15 @@ class BinaryBridge {
14595
16313
  if (this.process) {
14596
16314
  const proc = this.process;
14597
16315
  this.process = null;
14598
- return new Promise((resolve) => {
16316
+ return new Promise((resolve3) => {
14599
16317
  const forceKillTimer = setTimeout(() => {
14600
16318
  proc.kill("SIGKILL");
14601
- resolve();
16319
+ resolve3();
14602
16320
  }, 5000);
14603
16321
  proc.once("exit", () => {
14604
16322
  clearTimeout(forceKillTimer);
14605
16323
  log("Process exited during shutdown");
14606
- resolve();
16324
+ resolve3();
14607
16325
  });
14608
16326
  proc.kill("SIGTERM");
14609
16327
  });
@@ -14645,19 +16363,19 @@ class BinaryBridge {
14645
16363
  })();
14646
16364
  const useFastembedBackend = semanticBackend === undefined || semanticBackend === "fastembed" || semanticBackend === "";
14647
16365
  const ortDir = typeof this.configOverrides._ort_dylib_dir === "string" && useFastembedBackend ? this.configOverrides._ort_dylib_dir : null;
14648
- const ortLibraryPath = ortDir == null ? null : join5(ortDir, process.platform === "win32" ? "onnxruntime.dll" : process.platform === "darwin" ? "libonnxruntime.dylib" : "libonnxruntime.so");
16366
+ const ortLibraryPath = ortDir == null ? null : join8(ortDir, process.platform === "win32" ? "onnxruntime.dll" : process.platform === "darwin" ? "libonnxruntime.dylib" : "libonnxruntime.so");
14649
16367
  const envPath = process.platform === "win32" && ortDir ? `${ortDir};${process.env.PATH ?? ""}` : process.env.PATH;
14650
16368
  const env = {
14651
16369
  ...process.env,
14652
16370
  ...envPath ? { PATH: envPath } : {}
14653
16371
  };
14654
16372
  if (useFastembedBackend) {
14655
- env.FASTEMBED_CACHE_DIR = process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join5(this.configOverrides.storage_dir, "semantic", "models") : join5(homedir2() || "", ".cache", "fastembed"));
16373
+ env.FASTEMBED_CACHE_DIR = process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join8(this.configOverrides.storage_dir, "semantic", "models") : join8(homedir3() || "", ".cache", "fastembed"));
14656
16374
  if (ortLibraryPath) {
14657
16375
  env.ORT_DYLIB_PATH = ortLibraryPath;
14658
16376
  }
14659
16377
  }
14660
- const child = spawn(this.binaryPath, [], {
16378
+ const child = spawn2(this.binaryPath, [], {
14661
16379
  cwd: this.cwd,
14662
16380
  stdio: ["pipe", "pipe", "pipe"],
14663
16381
  env
@@ -14915,7 +16633,7 @@ class BridgePool {
14915
16633
  function canonicalKey(directory) {
14916
16634
  const stripped = directory.replace(/[/\\]+$/, "");
14917
16635
  try {
14918
- return realpathSync(stripped);
16636
+ return realpathSync3(stripped);
14919
16637
  } catch {
14920
16638
  return stripped;
14921
16639
  }
@@ -14923,15 +16641,15 @@ function canonicalKey(directory) {
14923
16641
 
14924
16642
  // src/resolver.ts
14925
16643
  import { execSync, spawnSync } from "node:child_process";
14926
- import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync } from "node:fs";
16644
+ import { chmodSync as chmodSync3, copyFileSync as copyFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, renameSync as renameSync2 } from "node:fs";
14927
16645
  import { createRequire as createRequire2 } from "node:module";
14928
- import { homedir as homedir4 } from "node:os";
14929
- import { join as join7 } from "node:path";
16646
+ import { homedir as homedir5 } from "node:os";
16647
+ import { join as join10 } from "node:path";
14930
16648
 
14931
16649
  // src/downloader.ts
14932
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
14933
- import { homedir as homedir3 } from "node:os";
14934
- import { join as join6 } from "node:path";
16650
+ import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
16651
+ import { homedir as homedir4 } from "node:os";
16652
+ import { join as join9 } from "node:path";
14935
16653
 
14936
16654
  // src/platform.ts
14937
16655
  var PLATFORM_ARCH_MAP = {
@@ -14952,11 +16670,11 @@ var REPO = "cortexkit/aft";
14952
16670
  function getCacheDir() {
14953
16671
  if (process.platform === "win32") {
14954
16672
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
14955
- const base2 = localAppData || join6(homedir3(), "AppData", "Local");
14956
- return join6(base2, "aft", "bin");
16673
+ const base2 = localAppData || join9(homedir4(), "AppData", "Local");
16674
+ return join9(base2, "aft", "bin");
14957
16675
  }
14958
- const base = process.env.XDG_CACHE_HOME || join6(homedir3(), ".cache");
14959
- return join6(base, "aft", "bin");
16676
+ const base = process.env.XDG_CACHE_HOME || join9(homedir4(), ".cache");
16677
+ return join9(base, "aft", "bin");
14960
16678
  }
14961
16679
  function getBinaryName() {
14962
16680
  return process.platform === "win32" ? "aft.exe" : "aft";
@@ -14964,8 +16682,8 @@ function getBinaryName() {
14964
16682
  function getCachedBinaryPath(version2) {
14965
16683
  if (!version2)
14966
16684
  return null;
14967
- const binaryPath = join6(getCacheDir(), version2, getBinaryName());
14968
- return existsSync4(binaryPath) ? binaryPath : null;
16685
+ const binaryPath = join9(getCacheDir(), version2, getBinaryName());
16686
+ return existsSync6(binaryPath) ? binaryPath : null;
14969
16687
  }
14970
16688
  async function downloadBinary(version2) {
14971
16689
  const platformKey = `${process.platform}-${process.arch}`;
@@ -14979,18 +16697,18 @@ async function downloadBinary(version2) {
14979
16697
  error48("Could not determine latest release version.");
14980
16698
  return null;
14981
16699
  }
14982
- const versionedCacheDir = join6(getCacheDir(), tag);
16700
+ const versionedCacheDir = join9(getCacheDir(), tag);
14983
16701
  const binaryName = getBinaryName();
14984
- const binaryPath = join6(versionedCacheDir, binaryName);
14985
- if (existsSync4(binaryPath)) {
16702
+ const binaryPath = join9(versionedCacheDir, binaryName);
16703
+ if (existsSync6(binaryPath)) {
14986
16704
  return binaryPath;
14987
16705
  }
14988
16706
  const downloadUrl = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
14989
16707
  const checksumUrl = `https://github.com/${REPO}/releases/download/${tag}/checksums.sha256`;
14990
16708
  log(`Downloading AFT binary (${tag}) for ${platformKey}...`);
14991
16709
  try {
14992
- if (!existsSync4(versionedCacheDir)) {
14993
- mkdirSync3(versionedCacheDir, { recursive: true });
16710
+ if (!existsSync6(versionedCacheDir)) {
16711
+ mkdirSync5(versionedCacheDir, { recursive: true });
14994
16712
  }
14995
16713
  const [binaryResponse, checksumResponse] = await Promise.all([
14996
16714
  fetch(downloadUrl, { redirect: "follow" }),
@@ -15010,29 +16728,29 @@ async function downloadBinary(version2) {
15010
16728
  warn(`Checksum verification failed: checksums.sha256 found but no entry for ${assetName}. ` + "Binary download aborted for security reasons.");
15011
16729
  return null;
15012
16730
  }
15013
- const { createHash } = await import("node:crypto");
15014
- const actualHash = createHash("sha256").update(Buffer.from(arrayBuffer)).digest("hex");
16731
+ const { createHash: createHash4 } = await import("node:crypto");
16732
+ const actualHash = createHash4("sha256").update(Buffer.from(arrayBuffer)).digest("hex");
15015
16733
  if (actualHash !== expectedHash) {
15016
16734
  throw new Error(`Checksum mismatch for ${assetName}: expected ${expectedHash}, got ${actualHash}. The binary may have been tampered with.`);
15017
16735
  }
15018
16736
  log(`Checksum verified (SHA-256: ${actualHash.slice(0, 16)}...)`);
15019
16737
  const tmpPath = `${binaryPath}.tmp`;
15020
- const { writeFileSync: writeFileSync2 } = await import("node:fs");
15021
- writeFileSync2(tmpPath, Buffer.from(arrayBuffer));
16738
+ const { writeFileSync: writeFileSync4 } = await import("node:fs");
16739
+ writeFileSync4(tmpPath, Buffer.from(arrayBuffer));
15022
16740
  if (process.platform !== "win32") {
15023
16741
  chmodSync2(tmpPath, 493);
15024
16742
  }
15025
- const { renameSync } = await import("node:fs");
15026
- renameSync(tmpPath, binaryPath);
16743
+ const { renameSync: renameSync2 } = await import("node:fs");
16744
+ renameSync2(tmpPath, binaryPath);
15027
16745
  log(`AFT binary ready at ${binaryPath}`);
15028
16746
  return binaryPath;
15029
16747
  } catch (err) {
15030
16748
  const msg = err instanceof Error ? err.message : String(err);
15031
16749
  error48(`Failed to download AFT binary: ${msg}`);
15032
16750
  const tmpPath = `${binaryPath}.tmp`;
15033
- if (existsSync4(tmpPath)) {
16751
+ if (existsSync6(tmpPath)) {
15034
16752
  try {
15035
- unlinkSync2(tmpPath);
16753
+ unlinkSync4(tmpPath);
15036
16754
  } catch {}
15037
16755
  }
15038
16756
  return null;
@@ -15097,18 +16815,18 @@ function copyToVersionedCache(npmBinaryPath) {
15097
16815
  const version2 = rawVersion.replace(/^aft\s+/, "");
15098
16816
  const tag = version2.startsWith("v") ? version2 : `v${version2}`;
15099
16817
  const cacheDir = getCacheDir();
15100
- const versionedDir = join7(cacheDir, tag);
16818
+ const versionedDir = join10(cacheDir, tag);
15101
16819
  const ext = process.platform === "win32" ? ".exe" : "";
15102
- const cachedPath = join7(versionedDir, `aft${ext}`);
15103
- if (existsSync5(cachedPath))
16820
+ const cachedPath = join10(versionedDir, `aft${ext}`);
16821
+ if (existsSync7(cachedPath))
15104
16822
  return cachedPath;
15105
- mkdirSync4(versionedDir, { recursive: true });
16823
+ mkdirSync6(versionedDir, { recursive: true });
15106
16824
  const tmpPath = `${cachedPath}.tmp`;
15107
- copyFileSync(npmBinaryPath, tmpPath);
16825
+ copyFileSync3(npmBinaryPath, tmpPath);
15108
16826
  if (process.platform !== "win32") {
15109
16827
  chmodSync3(tmpPath, 493);
15110
16828
  }
15111
- renameSync(tmpPath, cachedPath);
16829
+ renameSync2(tmpPath, cachedPath);
15112
16830
  log(`Copied npm binary to versioned cache: ${cachedPath}`);
15113
16831
  return cachedPath;
15114
16832
  } catch (err) {
@@ -15147,7 +16865,7 @@ function findBinarySync() {
15147
16865
  const packageBin = `@cortexkit/aft-${key}/bin/aft${ext}`;
15148
16866
  const req = createRequire2(import.meta.url);
15149
16867
  const resolved = req.resolve(packageBin);
15150
- if (existsSync5(resolved)) {
16868
+ if (existsSync7(resolved)) {
15151
16869
  const copied = copyToVersionedCache(resolved);
15152
16870
  return copied ?? resolved;
15153
16871
  }
@@ -15161,8 +16879,8 @@ function findBinarySync() {
15161
16879
  if (result)
15162
16880
  return result;
15163
16881
  } catch {}
15164
- const cargoPath = join7(homedir4(), ".cargo", "bin", `aft${ext}`);
15165
- if (existsSync5(cargoPath))
16882
+ const cargoPath = join10(homedir5(), ".cargo", "bin", `aft${ext}`);
16883
+ if (existsSync7(cargoPath))
15166
16884
  return cargoPath;
15167
16885
  return null;
15168
16886
  }
@@ -15252,7 +16970,7 @@ import { StringEnum } from "@mariozechner/pi-ai";
15252
16970
  import { Type } from "@sinclair/typebox";
15253
16971
 
15254
16972
  // src/tools/render-helpers.ts
15255
- import { homedir as homedir5 } from "node:os";
16973
+ import { homedir as homedir6 } from "node:os";
15256
16974
  import { renderDiff } from "@mariozechner/pi-coding-agent";
15257
16975
  import { Container, Spacer, Text } from "@mariozechner/pi-tui";
15258
16976
  function reuseText(last) {
@@ -15262,7 +16980,7 @@ function reuseContainer(last) {
15262
16980
  return last instanceof Container ? last : new Container;
15263
16981
  }
15264
16982
  function shortenPath(path2) {
15265
- const home = homedir5();
16983
+ const home = homedir6();
15266
16984
  if (path2.startsWith(home))
15267
16985
  return `~${path2.slice(home.length)}`;
15268
16986
  return path2;
@@ -15764,8 +17482,8 @@ function registerFsTools(pi, ctx, surface) {
15764
17482
 
15765
17483
  // src/tools/hoisted.ts
15766
17484
  import { stat } from "node:fs/promises";
15767
- import { homedir as homedir6 } from "node:os";
15768
- import { resolve } from "node:path";
17485
+ import { homedir as homedir7 } from "node:os";
17486
+ import { resolve as resolve3 } from "node:path";
15769
17487
  import {
15770
17488
  renderDiff as renderDiff2
15771
17489
  } from "@mariozechner/pi-coding-agent";
@@ -16118,13 +17836,13 @@ ${summary}${suffix}`);
16118
17836
  return container;
16119
17837
  }
16120
17838
  function shortenPath2(path2) {
16121
- const home = homedir6();
17839
+ const home = homedir7();
16122
17840
  if (path2.startsWith(home))
16123
17841
  return `~${path2.slice(home.length)}`;
16124
17842
  return path2;
16125
17843
  }
16126
17844
  async function resolvePathArg(cwd, path2) {
16127
- const abs = resolve(cwd, path2);
17845
+ const abs = resolve3(cwd, path2);
16128
17846
  try {
16129
17847
  await stat(abs);
16130
17848
  return abs;
@@ -16307,7 +18025,7 @@ function registerLspTools(pi, ctx) {
16307
18025
  pi.registerTool({
16308
18026
  name: "lsp_diagnostics",
16309
18027
  label: "lsp diagnostics",
16310
- description: "On-demand LSP file/scope check. Spawns the relevant language server (if registered for the extension), opens the document, prefers LSP 3.17 pull diagnostics where supported, falls back to push + waitMs otherwise. NOT a project-wide type checker — for full coverage run `tsc --noEmit`, `cargo check`, `pyright`, etc.\n\nResponse fields: `diagnostics`, `total`, `files_with_errors`, `complete` (true = trustable absence), `lsp_servers_used` (per-server status, e.g. `pull_ok`, `push_only`, `binary_not_installed: bash-language-server`, `no_root_marker (...)`), and (directory mode) `unchecked_files`.\n\nReading honestly: `total: 0` + empty `lsp_servers_used` means **nothing was checked** install the relevant LSP server. `total: 0` + `pull_ok` means the file is genuinely clean.\n\nProvide `filePath` for a single file, `directory` for files under a path (workspace pull from active servers + 200-file walk for unchecked listing), or omit both to dump cached diagnostics.",
18028
+ description: "On-demand LSP file/scope check. Spawns the relevant language server (if registered for the extension), opens the document, prefers LSP 3.17 pull diagnostics where supported, falls back to push + waitMs otherwise. NOT a project-wide type checker — for full coverage run `tsc --noEmit`, `cargo check`, `pyright`, etc.\n\nResponse fields: `diagnostics`, `total`, `files_with_errors`, `complete` (true = trustable absence), `lsp_servers_used` (per-server status, e.g. `pull_ok`, `push_only`, `binary_not_installed: <name>`, `no_root_marker (...)`), and (directory mode) `unchecked_files`.\n\nReading honestly:\n- `total: 0` + empty `lsp_servers_used` **nothing was checked** (no server registered for this extension). Tell the user, don't claim 'no errors'.\n- `total: 0` + `pull_ok` the file is genuinely clean.\n- `binary_not_installed: <name>` → server matched the extension but its binary isn't on PATH. Tell the user to install it.\n- `no_root_marker (...)` → server is registered but couldn't find a workspace root marker. The user's project layout doesn't match what the server expects.\n\nProvide `filePath` for a single file, `directory` for files under a path (workspace pull from active servers + 200-file walk for unchecked listing), or omit both to dump cached diagnostics.\n\n**When this tool gives an unhelpful answer**, run `bunx --bun @cortexkit/aft doctor lsp <filePath>` from a terminal to get a full per-server breakdown (registered servers, binary resolution, root-marker resolution, spawn outcome).",
16311
18029
  parameters: LspDiagnosticsParams,
16312
18030
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
16313
18031
  const hasFile = typeof params.filePath === "string" && params.filePath.length > 0;
@@ -16498,12 +18216,12 @@ function registerNavigateTool(pi, ctx) {
16498
18216
 
16499
18217
  // src/tools/reading.ts
16500
18218
  import { stat as stat2 } from "node:fs/promises";
16501
- import { resolve as resolve2 } from "node:path";
18219
+ import { resolve as resolve4 } from "node:path";
16502
18220
  import { Type as Type8 } from "@sinclair/typebox";
16503
18221
 
16504
18222
  // src/shared/discover-files.ts
16505
18223
  import { readdir } from "node:fs/promises";
16506
- import { extname, join as join8 } from "node:path";
18224
+ import { extname, join as join11 } from "node:path";
16507
18225
  var OUTLINE_EXTENSIONS = new Set([
16508
18226
  ".ts",
16509
18227
  ".tsx",
@@ -16575,12 +18293,12 @@ async function discoverSourceFiles(dir, maxFiles = 200) {
16575
18293
  return;
16576
18294
  if (entry.isDirectory()) {
16577
18295
  if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
16578
- await walk(join8(current, entry.name));
18296
+ await walk(join11(current, entry.name));
16579
18297
  }
16580
18298
  } else if (entry.isFile()) {
16581
18299
  const ext = extname(entry.name).toLowerCase();
16582
18300
  if (OUTLINE_EXTENSIONS.has(ext)) {
16583
- files.push(join8(current, entry.name));
18301
+ files.push(join11(current, entry.name));
16584
18302
  }
16585
18303
  }
16586
18304
  }
@@ -16691,14 +18409,14 @@ function registerReadingTools(pi, ctx, surface) {
16691
18409
  let dirArg = hasDirectory ? params.directory : undefined;
16692
18410
  if (!dirArg && hasFilePath) {
16693
18411
  try {
16694
- const resolved = resolve2(extCtx.cwd, params.filePath);
18412
+ const resolved = resolve4(extCtx.cwd, params.filePath);
16695
18413
  const st = await stat2(resolved);
16696
18414
  if (st.isDirectory())
16697
18415
  dirArg = params.filePath;
16698
18416
  } catch {}
16699
18417
  }
16700
18418
  if (dirArg) {
16701
- const dirPath = resolve2(extCtx.cwd, dirArg);
18419
+ const dirPath = resolve4(extCtx.cwd, dirArg);
16702
18420
  const files = await discoverSourceFiles(dirPath);
16703
18421
  if (files.length === 0) {
16704
18422
  return textResult(`No source files found under ${dirArg}`);
@@ -17272,7 +18990,7 @@ function coerceConfigureWarnings(warnings) {
17272
18990
  return warnings.filter(isConfigureWarning);
17273
18991
  }
17274
18992
  function resolveStorageDir() {
17275
- return join9(homedir7(), ".pi", "agent", "aft");
18993
+ return join12(homedir8(), ".pi", "agent", "aft");
17276
18994
  }
17277
18995
  function resolveToolSurface(config2) {
17278
18996
  const surface = config2.tool_surface ?? "recommended";
@@ -17355,16 +19073,85 @@ async function src_default(pi) {
17355
19073
  warn(`Failed to prepare ONNX Runtime: ${err instanceof Error ? err.message : String(err)}`);
17356
19074
  }
17357
19075
  }
17358
- const configOverrides = {
17359
- ...config2,
17360
- ...resolveLspConfigForConfigure(config2),
17361
- restrict_to_project_root: config2.restrict_to_project_root ?? true,
17362
- storage_dir: storageDir
17363
- };
17364
- delete configOverrides.lsp;
19076
+ const configOverrides = {};
19077
+ if (config2.format_on_edit !== undefined)
19078
+ configOverrides.format_on_edit = config2.format_on_edit;
19079
+ if (config2.validate_on_edit !== undefined)
19080
+ configOverrides.validate_on_edit = config2.validate_on_edit;
19081
+ if (config2.formatter !== undefined)
19082
+ configOverrides.formatter = config2.formatter;
19083
+ if (config2.checker !== undefined)
19084
+ configOverrides.checker = config2.checker;
19085
+ configOverrides.restrict_to_project_root = config2.restrict_to_project_root ?? true;
19086
+ if (config2.experimental_search_index !== undefined)
19087
+ configOverrides.experimental_search_index = config2.experimental_search_index;
19088
+ if (config2.experimental_semantic_search !== undefined)
19089
+ configOverrides.experimental_semantic_search = config2.experimental_semantic_search;
19090
+ Object.assign(configOverrides, resolveLspConfigForConfigure(config2));
19091
+ if (config2.semantic !== undefined)
19092
+ configOverrides.semantic = config2.semantic;
19093
+ if (config2.max_callgraph_files !== undefined)
19094
+ configOverrides.max_callgraph_files = config2.max_callgraph_files;
19095
+ if (config2.url_fetch_allow_private !== undefined)
19096
+ configOverrides.url_fetch_allow_private = config2.url_fetch_allow_private;
19097
+ configOverrides.storage_dir = storageDir;
17365
19098
  if (ortDylibDir) {
17366
19099
  configOverrides._ort_dylib_dir = ortDylibDir;
17367
19100
  }
19101
+ try {
19102
+ const lspAutoInstall = config2.lsp?.auto_install ?? true;
19103
+ const lspGraceDays = config2.lsp?.grace_days ?? 7;
19104
+ const lspVersions = config2.lsp?.versions ?? {};
19105
+ const lspDisabled = new Set(config2.lsp?.disabled ?? []);
19106
+ const projectRoot = process.cwd();
19107
+ const npmResult = runAutoInstall(projectRoot, {
19108
+ autoInstall: lspAutoInstall,
19109
+ graceDays: lspGraceDays,
19110
+ versions: lspVersions,
19111
+ disabled: lspDisabled
19112
+ });
19113
+ const relevantGithub = discoverRelevantGithubServers(projectRoot);
19114
+ const ghResult = runGithubAutoInstall(relevantGithub, {
19115
+ autoInstall: lspAutoInstall,
19116
+ graceDays: lspGraceDays,
19117
+ versions: lspVersions,
19118
+ disabled: lspDisabled
19119
+ });
19120
+ const mergedBinDirs = [...npmResult.cachedBinDirs, ...ghResult.cachedBinDirs];
19121
+ if (mergedBinDirs.length > 0) {
19122
+ configOverrides.lsp_paths_extra = mergedBinDirs;
19123
+ }
19124
+ if (npmResult.installsStarted > 0 || ghResult.installsStarted > 0) {
19125
+ log(`[lsp] auto-install: ${npmResult.installsStarted} npm + ${ghResult.installsStarted} github install(s) running in background`);
19126
+ }
19127
+ Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
19128
+ const actionable = [...npmResult.skipped, ...ghResult.skipped].filter((s) => {
19129
+ const r = s.reason.toLowerCase();
19130
+ if (r === "auto_install: false")
19131
+ return false;
19132
+ if (r === "disabled by config")
19133
+ return false;
19134
+ if (r === "not relevant to project")
19135
+ return false;
19136
+ if (r === "already installed")
19137
+ return false;
19138
+ if (r === "another install in progress")
19139
+ return false;
19140
+ return true;
19141
+ });
19142
+ if (actionable.length === 0)
19143
+ return;
19144
+ const lines = actionable.map((s) => ` • ${s.id}: ${s.reason}`).join(`
19145
+ `);
19146
+ warn(`[lsp] skipped or failed to install ${actionable.length} server(s):
19147
+ ${lines}
19148
+ ` + 'Pin a working version with `lsp.versions: { "<package>": "<version>" }` if grace is blocking, ' + "or set `lsp.auto_install: false` to suppress.");
19149
+ }).catch((err) => {
19150
+ warn(`[lsp] install-summary aggregation failed: ${err}`);
19151
+ });
19152
+ } catch (err) {
19153
+ warn(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
19154
+ }
17368
19155
  const pool = new BridgePool(binaryPath, {
17369
19156
  minVersion: PLUGIN_VERSION,
17370
19157
  onConfigureWarnings: async ({ projectRoot, sessionId, client, warnings }) => {
@@ -17421,6 +19208,7 @@ async function src_default(pi) {
17421
19208
  registerStatusCommand(pi, ctx);
17422
19209
  pi.on("session_shutdown", async () => {
17423
19210
  try {
19211
+ await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
17424
19212
  await pool.shutdown();
17425
19213
  log("Bridge pool shut down");
17426
19214
  } catch (err) {
@@ -17429,6 +19217,7 @@ async function src_default(pi) {
17429
19217
  });
17430
19218
  registerShutdownCleanup(async () => {
17431
19219
  try {
19220
+ await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
17432
19221
  await pool.shutdown();
17433
19222
  } catch (err) {
17434
19223
  warn(`Error during process shutdown: ${err instanceof Error ? err.message : String(err)}`);