@cortexkit/aft-opencode 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.
Files changed (34) hide show
  1. package/dist/config.d.ts +4 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/hooks/auto-update-checker/cache.d.ts +12 -0
  4. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -0
  5. package/dist/hooks/auto-update-checker/checker.d.ts +13 -0
  6. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -0
  7. package/dist/hooks/auto-update-checker/constants.d.ts +10 -0
  8. package/dist/hooks/auto-update-checker/constants.d.ts.map +1 -0
  9. package/dist/hooks/auto-update-checker/index.d.ts +12 -0
  10. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -0
  11. package/dist/hooks/auto-update-checker/types.d.ts +31 -0
  12. package/dist/hooks/auto-update-checker/types.d.ts.map +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2641 -336
  15. package/dist/lsp-auto-install.d.ts +82 -0
  16. package/dist/lsp-auto-install.d.ts.map +1 -0
  17. package/dist/lsp-cache.d.ts +116 -0
  18. package/dist/lsp-cache.d.ts.map +1 -0
  19. package/dist/lsp-github-install.d.ts +99 -0
  20. package/dist/lsp-github-install.d.ts.map +1 -0
  21. package/dist/lsp-github-probe.d.ts +81 -0
  22. package/dist/lsp-github-probe.d.ts.map +1 -0
  23. package/dist/lsp-github-table.d.ts +62 -0
  24. package/dist/lsp-github-table.d.ts.map +1 -0
  25. package/dist/lsp-npm-table.d.ts +31 -0
  26. package/dist/lsp-npm-table.d.ts.map +1 -0
  27. package/dist/lsp-project-relevance.d.ts +11 -0
  28. package/dist/lsp-project-relevance.d.ts.map +1 -0
  29. package/dist/lsp-registry-probe.d.ts +50 -0
  30. package/dist/lsp-registry-probe.d.ts.map +1 -0
  31. package/dist/onnx-runtime.d.ts +21 -3
  32. package/dist/onnx-runtime.d.ts.map +1 -1
  33. package/dist/tools/lsp.d.ts.map +1 -1
  34. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -7803,10 +7803,10 @@ var require_src2 = __commonJS((exports, module) => {
7803
7803
  });
7804
7804
 
7805
7805
  // src/index.ts
7806
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
7806
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
7807
7807
  import { createRequire as createRequire2 } from "module";
7808
- import { homedir as homedir7 } from "os";
7809
- import { join as join13 } from "path";
7808
+ import { homedir as homedir10 } from "os";
7809
+ import { join as join19 } from "path";
7810
7810
 
7811
7811
  // src/config.ts
7812
7812
  var import_comment_json = __toESM(require_src2(), 1);
@@ -21461,7 +21461,10 @@ var LspServerSchema = LspServerEntrySchema.extend({
21461
21461
  var LspConfigSchema = exports_external.object({
21462
21462
  servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
21463
21463
  disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
21464
- python: exports_external.enum(["pyright", "ty", "auto"]).optional()
21464
+ python: exports_external.enum(["pyright", "ty", "auto"]).optional(),
21465
+ auto_install: exports_external.boolean().optional(),
21466
+ grace_days: exports_external.number().int().positive().optional(),
21467
+ versions: exports_external.record(exports_external.string().trim().min(1), exports_external.string().trim().min(1)).optional()
21465
21468
  });
21466
21469
  var AftConfigSchema = exports_external.object({
21467
21470
  format_on_edit: exports_external.boolean().optional(),
@@ -21478,7 +21481,8 @@ var AftConfigSchema = exports_external.object({
21478
21481
  lsp: LspConfigSchema.optional(),
21479
21482
  url_fetch_allow_private: exports_external.boolean().optional(),
21480
21483
  semantic: SemanticConfigSchema.optional(),
21481
- max_callgraph_files: exports_external.number().int().positive().optional()
21484
+ max_callgraph_files: exports_external.number().int().positive().optional(),
21485
+ auto_update: exports_external.boolean().optional()
21482
21486
  });
21483
21487
  function normalizeLspExtension(extension) {
21484
21488
  return extension.trim().replace(/^\.+/, "");
@@ -21604,26 +21608,81 @@ function mergeSemanticConfig(baseSemantic, overrideSemantic) {
21604
21608
  }
21605
21609
  return Object.fromEntries(Object.entries(semantic).filter(([, value]) => value !== undefined));
21606
21610
  }
21611
+ function mergeLspConfig(baseLsp, overrideLsp) {
21612
+ const projectLsp = {};
21613
+ if (overrideLsp?.python !== undefined)
21614
+ projectLsp.python = overrideLsp.python;
21615
+ const userDisabled = baseLsp?.disabled ?? [];
21616
+ const lsp = {
21617
+ ...baseLsp,
21618
+ ...projectLsp,
21619
+ ...userDisabled.length > 0 ? { disabled: [...userDisabled] } : {}
21620
+ };
21621
+ if (Object.values(lsp).every((value) => value === undefined)) {
21622
+ return;
21623
+ }
21624
+ return Object.fromEntries(Object.entries(lsp).filter(([, value]) => value !== undefined));
21625
+ }
21626
+ function getProjectLspStrippedKeys(lsp) {
21627
+ if (!lsp) {
21628
+ return [];
21629
+ }
21630
+ const strippedKeys = [];
21631
+ if (lsp.servers !== undefined)
21632
+ strippedKeys.push("lsp.servers");
21633
+ if (lsp.versions !== undefined)
21634
+ strippedKeys.push("lsp.versions");
21635
+ if (lsp.auto_install !== undefined)
21636
+ strippedKeys.push("lsp.auto_install");
21637
+ if (lsp.grace_days !== undefined)
21638
+ strippedKeys.push("lsp.grace_days");
21639
+ if (lsp.disabled !== undefined)
21640
+ strippedKeys.push("lsp.disabled");
21641
+ return strippedKeys;
21642
+ }
21643
+ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
21644
+ "tool_surface",
21645
+ "hoist_builtin_tools",
21646
+ "format_on_edit",
21647
+ "validate_on_edit",
21648
+ "experimental_search_index",
21649
+ "experimental_semantic_search",
21650
+ "experimental_lsp_ty"
21651
+ ]);
21652
+ function pickProjectSafeFields(override) {
21653
+ const safe = {};
21654
+ for (const key of PROJECT_SAFE_TOP_LEVEL_FIELDS) {
21655
+ if (override[key] !== undefined) {
21656
+ safe[key] = override[key];
21657
+ }
21658
+ }
21659
+ return safe;
21660
+ }
21661
+ function getStrippedTopLevelKeys(override) {
21662
+ const stripped = [];
21663
+ if (override.restrict_to_project_root !== undefined)
21664
+ stripped.push("restrict_to_project_root");
21665
+ if (override.url_fetch_allow_private !== undefined)
21666
+ stripped.push("url_fetch_allow_private");
21667
+ if (override.max_callgraph_files !== undefined)
21668
+ stripped.push("max_callgraph_files");
21669
+ if (override.auto_update !== undefined)
21670
+ stripped.push("auto_update");
21671
+ return stripped;
21672
+ }
21607
21673
  function mergeConfigs(base, override) {
21608
21674
  const disabledTools = [...base.disabled_tools ?? [], ...override.disabled_tools ?? []];
21609
21675
  const formatter = { ...base.formatter, ...override.formatter };
21610
21676
  const checker = { ...base.checker, ...override.checker };
21611
21677
  const semantic = mergeSemanticConfig(base.semantic, override.semantic);
21612
- const lspServers = { ...base.lsp?.servers, ...override.lsp?.servers };
21613
- const disabledLsp = [...base.lsp?.disabled ?? [], ...override.lsp?.disabled ?? []];
21614
- const lsp = {
21615
- ...base.lsp,
21616
- ...override.lsp,
21617
- ...Object.keys(lspServers).length > 0 ? { servers: lspServers } : {},
21618
- ...disabledLsp.length > 0 ? { disabled: [...new Set(disabledLsp)] } : {}
21619
- };
21620
- const { semantic: _stripSemantic, ...safeOverride } = override;
21678
+ const lsp = mergeLspConfig(base.lsp, override.lsp);
21679
+ const safeOverride = pickProjectSafeFields(override);
21621
21680
  return {
21622
21681
  ...base,
21623
21682
  ...safeOverride,
21624
21683
  ...Object.keys(formatter).length > 0 ? { formatter } : {},
21625
21684
  ...Object.keys(checker).length > 0 ? { checker } : {},
21626
- ...Object.values(lsp).some((value) => value !== undefined) ? { lsp } : {},
21685
+ ...lsp ? { lsp } : {},
21627
21686
  semantic,
21628
21687
  ...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
21629
21688
  };
@@ -21650,6 +21709,14 @@ function loadAftConfig(projectDirectory) {
21650
21709
  if (projectConfig.semantic?.backend !== undefined || projectConfig.semantic?.base_url !== undefined || projectConfig.semantic?.api_key_env !== undefined) {
21651
21710
  warn("Ignoring semantic.backend/base_url/api_key_env from project config (security: use user config for external backends)");
21652
21711
  }
21712
+ const strippedLspKeys = getProjectLspStrippedKeys(projectConfig.lsp);
21713
+ if (strippedLspKeys.length > 0) {
21714
+ warn(`Ignoring ${strippedLspKeys.join(", ")} from project config ${projectConfigPath} (security: these LSP settings only honor user-level config)`);
21715
+ }
21716
+ const strippedTopLevelKeys = getStrippedTopLevelKeys(projectConfig);
21717
+ if (strippedTopLevelKeys.length > 0) {
21718
+ warn(`Ignoring ${strippedTopLevelKeys.join(", ")} from project config ${projectConfigPath} (security: these settings only honor user-level config \u2014 a project should not weaken security boundaries for the user)`);
21719
+ }
21653
21720
  config2 = mergeConfigs(config2, projectConfig);
21654
21721
  }
21655
21722
  return config2;
@@ -21805,194 +21872,2115 @@ async function fetchLatestTag() {
21805
21872
  }
21806
21873
  }
21807
21874
 
21808
- // src/metadata-store.ts
21809
- var pendingStore = new Map;
21810
- var STALE_TIMEOUT_MS = 15 * 60 * 1000;
21811
- function makeKey(sessionID, callID) {
21812
- return `${sessionID}:${callID}`;
21813
- }
21814
- function cleanupStaleEntries() {
21815
- const now = Date.now();
21816
- for (const [key, entry] of pendingStore) {
21817
- if (now - entry.storedAt > STALE_TIMEOUT_MS) {
21818
- pendingStore.delete(key);
21819
- }
21875
+ // src/hooks/auto-update-checker/cache.ts
21876
+ var import_comment_json3 = __toESM(require_src2(), 1);
21877
+ import { spawn } from "child_process";
21878
+ import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
21879
+ import { basename, dirname as dirname2, join as join6 } from "path";
21880
+
21881
+ // src/hooks/auto-update-checker/checker.ts
21882
+ var import_comment_json2 = __toESM(require_src2(), 1);
21883
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
21884
+ import { homedir as homedir4 } from "os";
21885
+ import { dirname, isAbsolute, join as join5, resolve } from "path";
21886
+ import { fileURLToPath } from "url";
21887
+
21888
+ // src/hooks/auto-update-checker/constants.ts
21889
+ import { homedir as homedir3, platform } from "os";
21890
+ import { join as join4 } from "path";
21891
+ var PACKAGE_NAME = "@cortexkit/aft-opencode";
21892
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org";
21893
+ var NPM_FETCH_TIMEOUT = 1e4;
21894
+ function getOpenCodeCacheRoot() {
21895
+ if (platform() === "win32") {
21896
+ return join4(process.env.LOCALAPPDATA ?? homedir3(), "opencode");
21820
21897
  }
21898
+ return join4(homedir3(), ".cache", "opencode");
21821
21899
  }
21822
- function storeToolMetadata(sessionID, callID, data) {
21823
- cleanupStaleEntries();
21824
- pendingStore.set(makeKey(sessionID, callID), {
21825
- ...data,
21826
- storedAt: Date.now()
21827
- });
21828
- }
21829
- function consumeToolMetadata(sessionID, callID) {
21830
- const key = makeKey(sessionID, callID);
21831
- const stored = pendingStore.get(key);
21832
- if (stored) {
21833
- pendingStore.delete(key);
21834
- const { storedAt: _, ...data } = stored;
21835
- return data;
21900
+ function getOpenCodeConfigRoot() {
21901
+ if (platform() === "win32") {
21902
+ return join4(process.env.APPDATA ?? join4(homedir3(), "AppData", "Roaming"), "opencode");
21836
21903
  }
21837
- return;
21904
+ return join4(process.env.XDG_CONFIG_HOME ?? join4(homedir3(), ".config"), "opencode");
21838
21905
  }
21906
+ var CACHE_DIR = join4(getOpenCodeCacheRoot(), "packages");
21907
+ var USER_OPENCODE_CONFIG = join4(getOpenCodeConfigRoot(), "opencode.json");
21908
+ var USER_OPENCODE_CONFIG_JSONC = join4(getOpenCodeConfigRoot(), "opencode.jsonc");
21839
21909
 
21840
- // src/normalize-schemas.ts
21841
- import { tool } from "@opencode-ai/plugin";
21842
- function stripRootJsonSchemaFields(jsonSchema) {
21843
- const { $schema: _schema, ...rest } = jsonSchema;
21844
- return rest;
21910
+ // src/hooks/auto-update-checker/types.ts
21911
+ var NpmPackageEnvelopeSchema = exports_external.object({
21912
+ "dist-tags": exports_external.record(exports_external.string(), exports_external.string()).optional().default({})
21913
+ });
21914
+ var OpencodePluginTupleSchema = exports_external.tuple([exports_external.string(), exports_external.record(exports_external.string(), exports_external.unknown())]);
21915
+ var OpencodeConfigSchema = exports_external.object({
21916
+ plugin: exports_external.array(exports_external.union([exports_external.string(), OpencodePluginTupleSchema])).optional()
21917
+ });
21918
+ var PackageJsonSchema = exports_external.object({
21919
+ name: exports_external.string().optional(),
21920
+ version: exports_external.string().optional(),
21921
+ dependencies: exports_external.record(exports_external.string(), exports_external.string()).optional()
21922
+ }).passthrough();
21923
+
21924
+ // src/hooks/auto-update-checker/checker.ts
21925
+ function isString(value) {
21926
+ return typeof value === "string";
21845
21927
  }
21846
- function attachJsonSchemaOverride(schema) {
21847
- if (schema._zod.toJSONSchema) {
21848
- return;
21928
+ function pluginSpecifier(entry) {
21929
+ return typeof entry === "string" ? entry : entry[0];
21930
+ }
21931
+ function getPluginEntries(config2) {
21932
+ const parsed = OpencodeConfigSchema.safeParse(config2);
21933
+ if (!parsed.success)
21934
+ return [];
21935
+ return (parsed.data.plugin ?? []).map(pluginSpecifier).filter(isString);
21936
+ }
21937
+ function parseJsonConfig(content) {
21938
+ try {
21939
+ return import_comment_json2.parse(content);
21940
+ } catch (err) {
21941
+ warn(`[auto-update-checker] Failed to parse OpenCode config: ${String(err)}`);
21942
+ return null;
21849
21943
  }
21850
- schema._zod.toJSONSchema = () => {
21851
- const originalOverride = schema._zod.toJSONSchema;
21852
- delete schema._zod.toJSONSchema;
21944
+ }
21945
+ function isPrereleaseVersion(version2) {
21946
+ return version2.includes("-");
21947
+ }
21948
+ function isDistTag(version2) {
21949
+ return !/^\d/.test(version2);
21950
+ }
21951
+ function extractChannel(version2) {
21952
+ if (!version2)
21953
+ return "latest";
21954
+ if (isDistTag(version2))
21955
+ return version2;
21956
+ if (isPrereleaseVersion(version2)) {
21957
+ const prereleasePart = version2.split("-")[1];
21958
+ const channelMatch = prereleasePart?.match(/^(alpha|beta|rc|canary|next)/);
21959
+ if (channelMatch?.[1])
21960
+ return channelMatch[1];
21961
+ }
21962
+ return "latest";
21963
+ }
21964
+ function getConfigPaths(directory) {
21965
+ return [
21966
+ join5(directory, ".opencode", "opencode.json"),
21967
+ join5(directory, ".opencode", "opencode.jsonc"),
21968
+ USER_OPENCODE_CONFIG,
21969
+ USER_OPENCODE_CONFIG_JSONC
21970
+ ];
21971
+ }
21972
+ function resolvePathPluginSpec(spec, configPath) {
21973
+ if (spec.startsWith("file://")) {
21853
21974
  try {
21854
- return stripRootJsonSchemaFields(tool.schema.toJSONSchema(schema));
21855
- } finally {
21856
- schema._zod.toJSONSchema = originalOverride;
21975
+ return fileURLToPath(spec);
21976
+ } catch {
21977
+ return spec.replace(/^file:\/\//, "");
21857
21978
  }
21858
- };
21859
- }
21860
- function normalizeToolArgSchemas(toolDefinition) {
21861
- for (const schema of Object.values(toolDefinition.args)) {
21862
- attachJsonSchemaOverride(schema);
21863
21979
  }
21864
- return toolDefinition;
21980
+ if (isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec))
21981
+ return spec;
21982
+ return resolve(dirname(configPath), spec);
21865
21983
  }
21866
- function normalizeToolMap(tools) {
21867
- for (const def of Object.values(tools)) {
21868
- normalizeToolArgSchemas(def);
21984
+ function getLocalDevPath(directory) {
21985
+ for (const configPath of getConfigPaths(directory)) {
21986
+ try {
21987
+ if (!existsSync3(configPath))
21988
+ continue;
21989
+ const rawConfig = parseJsonConfig(readFileSync2(configPath, "utf-8"));
21990
+ const plugins = getPluginEntries(rawConfig);
21991
+ for (const entry of plugins) {
21992
+ if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`))
21993
+ continue;
21994
+ if (entry.startsWith("file://") || entry.startsWith(".") || isAbsolute(entry)) {
21995
+ const localPath = resolvePathPluginSpec(entry, configPath);
21996
+ const pkgPath = findPackageJsonUp(localPath);
21997
+ if (!pkgPath)
21998
+ continue;
21999
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync2(pkgPath, "utf-8")));
22000
+ if (pkg.success && pkg.data.name === PACKAGE_NAME)
22001
+ return localPath;
22002
+ }
22003
+ }
22004
+ } catch {}
21869
22005
  }
21870
- return tools;
22006
+ return null;
21871
22007
  }
21872
-
21873
- // src/notifications.ts
21874
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
21875
- import { homedir as homedir3, platform } from "os";
21876
- import { join as join4 } from "path";
21877
- function isTuiMode() {
21878
- return process.env.OPENCODE_CLIENT === "cli";
22008
+ function findPackageJsonUp(startPath) {
22009
+ try {
22010
+ const stat = statSync(startPath);
22011
+ let dir = stat.isDirectory() ? startPath : dirname(startPath);
22012
+ for (let i = 0;i < 10; i++) {
22013
+ const pkgPath = join5(dir, "package.json");
22014
+ if (existsSync3(pkgPath)) {
22015
+ try {
22016
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync2(pkgPath, "utf-8")));
22017
+ if (pkg.success && pkg.data.name === PACKAGE_NAME)
22018
+ return pkgPath;
22019
+ } catch {}
22020
+ }
22021
+ const parent = dirname(dir);
22022
+ if (parent === dir)
22023
+ break;
22024
+ dir = parent;
22025
+ }
22026
+ } catch {}
22027
+ return null;
21879
22028
  }
21880
- async function showTuiToast(client, title, message, variant = "info", duration3 = 8000) {
21881
- const c = client;
21882
- if (typeof c.tui?.showToast !== "function")
21883
- return false;
22029
+ function getLocalDevVersion(directory) {
22030
+ const localPath = getLocalDevPath(directory);
22031
+ if (!localPath)
22032
+ return null;
21884
22033
  try {
21885
- await c.tui.showToast({ body: { title, message, variant, duration: duration3 } });
21886
- return true;
22034
+ const pkgPath = findPackageJsonUp(localPath);
22035
+ if (!pkgPath)
22036
+ return null;
22037
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync2(pkgPath, "utf-8")));
22038
+ return pkg.success ? pkg.data.version ?? null : null;
21887
22039
  } catch {
21888
- return false;
22040
+ return null;
21889
22041
  }
21890
22042
  }
21891
- var AFT_MARKER = "\uD83D\uDD27 AFT:";
21892
- var FEATURE_MARKER = `${AFT_MARKER} New in`;
21893
- var WARNING_MARKER = `${AFT_MARKER} \u26A0\uFE0F`;
21894
- var STATUS_MARKER = `${AFT_MARKER} \u2705`;
21895
- var WARNED_TOOLS_FILE = "warned_tools.json";
21896
- function getDesktopStatePath() {
21897
- const os2 = platform();
21898
- const home = homedir3();
21899
- if (os2 === "darwin") {
21900
- return join4(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
22043
+ function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
22044
+ try {
22045
+ return findPackageJsonUp(dirname(fileURLToPath(currentModuleUrl)));
22046
+ } catch (err) {
22047
+ warn(`[auto-update-checker] Failed to resolve runtime package path: ${String(err)}`);
22048
+ return null;
21901
22049
  }
21902
- if (os2 === "linux") {
21903
- const xdgConfig = process.env.XDG_CONFIG_HOME || join4(home, ".config");
21904
- return join4(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
22050
+ }
22051
+ function findPluginEntry(directory) {
22052
+ for (const configPath of getConfigPaths(directory)) {
22053
+ try {
22054
+ if (!existsSync3(configPath))
22055
+ continue;
22056
+ const rawConfig = parseJsonConfig(readFileSync2(configPath, "utf-8"));
22057
+ const plugins = getPluginEntries(rawConfig);
22058
+ for (const entry of plugins) {
22059
+ if (entry === PACKAGE_NAME) {
22060
+ return { entry, isPinned: false, pinnedVersion: null, configPath };
22061
+ }
22062
+ if (entry.startsWith(`${PACKAGE_NAME}@`)) {
22063
+ const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1);
22064
+ const isPinned = pinnedVersion !== "latest";
22065
+ return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null, configPath };
22066
+ }
22067
+ }
22068
+ } catch {}
21905
22069
  }
21906
- if (os2 === "win32") {
21907
- const appData = process.env.APPDATA || join4(home, "AppData", "Roaming");
21908
- return join4(appData, "ai.opencode.desktop", "opencode.global.dat");
22070
+ return null;
22071
+ }
22072
+ var cachedPackageVersion = null;
22073
+ function getSpecCachePackageJsonPath(spec) {
22074
+ return join5(CACHE_DIR, spec, "node_modules", PACKAGE_NAME, "package.json");
22075
+ }
22076
+ function getCachedVersion(spec) {
22077
+ if (!spec && cachedPackageVersion)
22078
+ return cachedPackageVersion;
22079
+ const candidates = [
22080
+ getCurrentRuntimePackageJsonPath(),
22081
+ spec ? getSpecCachePackageJsonPath(spec) : null,
22082
+ getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
22083
+ join5(homedir4(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
22084
+ ].filter(isString);
22085
+ for (const packageJsonPath of candidates) {
22086
+ try {
22087
+ if (!existsSync3(packageJsonPath))
22088
+ continue;
22089
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync2(packageJsonPath, "utf-8")));
22090
+ if (pkg.success && pkg.data.version) {
22091
+ if (!spec)
22092
+ cachedPackageVersion = pkg.data.version;
22093
+ return pkg.data.version;
22094
+ }
22095
+ } catch {}
21909
22096
  }
21910
22097
  return null;
21911
22098
  }
21912
- function readDesktopState(directory) {
21913
- const statePath = getDesktopStatePath();
21914
- if (!statePath || !existsSync3(statePath)) {
21915
- return { sessionId: null, serverUrl: null };
22099
+ function buildRegistryUrl(registryUrl) {
22100
+ return `${registryUrl.replace(/\/+$/, "")}/${encodeURIComponent(PACKAGE_NAME).replace("%2F", "/")}`;
22101
+ }
22102
+ async function getLatestVersion(channel = "latest", options = {}) {
22103
+ const controller = new AbortController;
22104
+ const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs ?? NPM_FETCH_TIMEOUT);
22105
+ const abortHandler = () => controller.abort();
22106
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
22107
+ try {
22108
+ if (options.signal?.aborted)
22109
+ return null;
22110
+ const response = await fetch(buildRegistryUrl(options.registryUrl ?? NPM_REGISTRY_URL), {
22111
+ signal: controller.signal,
22112
+ headers: { Accept: "application/json" }
22113
+ });
22114
+ if (!response.ok)
22115
+ return null;
22116
+ const data = NpmPackageEnvelopeSchema.safeParse(await response.json());
22117
+ if (!data.success)
22118
+ return null;
22119
+ return data.data["dist-tags"][channel] ?? data.data["dist-tags"].latest ?? null;
22120
+ } catch {
22121
+ return null;
22122
+ } finally {
22123
+ options.signal?.removeEventListener("abort", abortHandler);
22124
+ clearTimeout(timeoutId);
22125
+ }
22126
+ }
22127
+
22128
+ // src/hooks/auto-update-checker/cache.ts
22129
+ function stripPackageNameFromPath(pathValue, packageName) {
22130
+ let current = pathValue;
22131
+ for (const segment of [...packageName.split("/")].reverse()) {
22132
+ if (basename(current) !== segment)
22133
+ return null;
22134
+ current = dirname2(current);
21916
22135
  }
22136
+ return current;
22137
+ }
22138
+ function removeFromBunLock(installDir, packageName) {
22139
+ const lockPath = join6(installDir, "bun.lock");
22140
+ if (!existsSync4(lockPath))
22141
+ return false;
21917
22142
  try {
21918
- const raw = readFileSync2(statePath, "utf-8");
21919
- const state = JSON.parse(raw);
21920
- let serverUrl = null;
21921
- const serverStr = state.server;
21922
- if (typeof serverStr === "string") {
21923
- try {
21924
- const serverState = JSON.parse(serverStr);
21925
- if (typeof serverState.currentSidecarUrl === "string") {
21926
- serverUrl = serverState.currentSidecarUrl;
21927
- }
21928
- } catch {}
22143
+ const lock = import_comment_json3.parse(readFileSync3(lockPath, "utf-8"));
22144
+ let modified = false;
22145
+ if (lock.workspaces?.[""]?.dependencies?.[packageName]) {
22146
+ delete lock.workspaces[""].dependencies[packageName];
22147
+ modified = true;
21929
22148
  }
21930
- let sessionId = null;
21931
- const layoutPage = state["layout.page"];
21932
- if (typeof layoutPage === "string") {
21933
- const parsed = JSON.parse(layoutPage);
21934
- const lastProjectSession = parsed.lastProjectSession;
21935
- if (lastProjectSession) {
21936
- const entry = lastProjectSession[directory];
21937
- sessionId = entry?.id ?? null;
21938
- }
22149
+ if (lock.packages?.[packageName]) {
22150
+ delete lock.packages[packageName];
22151
+ modified = true;
21939
22152
  }
21940
- return { sessionId, serverUrl };
22153
+ if (modified) {
22154
+ writeFileSync2(lockPath, JSON.stringify(lock, null, 2));
22155
+ log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
22156
+ }
22157
+ return modified;
21941
22158
  } catch {
21942
- return { sessionId: null, serverUrl: null };
22159
+ return false;
21943
22160
  }
21944
22161
  }
21945
- function getServerAuth() {
21946
- const password = process.env.OPENCODE_SERVER_PASSWORD;
21947
- if (!password)
21948
- return;
21949
- const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
21950
- return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
21951
- }
21952
- async function getSessionMessages(client, sessionId) {
22162
+ function ensureDependencyVersion(packageJsonPath, packageName, version2) {
22163
+ if (!existsSync4(packageJsonPath))
22164
+ return false;
21953
22165
  try {
21954
- const c = client;
21955
- if (typeof c.session?.messages === "function") {
21956
- const result = await c.session.messages({ path: { id: sessionId } });
21957
- return result?.data ?? [];
22166
+ const raw = import_comment_json3.parse(readFileSync3(packageJsonPath, "utf-8"));
22167
+ const pkgJson = PackageJsonSchema.safeParse(raw);
22168
+ if (!pkgJson.success)
22169
+ return false;
22170
+ const nextPackageJson = { ...pkgJson.data };
22171
+ const dependencies = { ...nextPackageJson.dependencies ?? {} };
22172
+ if (dependencies[packageName] === version2)
22173
+ return true;
22174
+ dependencies[packageName] = version2;
22175
+ nextPackageJson.dependencies = dependencies;
22176
+ writeFileSync2(packageJsonPath, JSON.stringify(nextPackageJson, null, 2));
22177
+ log(`[auto-update-checker] Updated dependency in package.json: ${packageName} \u2192 ${version2}`);
22178
+ return true;
22179
+ } catch (err) {
22180
+ warn(`[auto-update-checker] Failed to update package.json dependency: ${String(err)}`);
22181
+ return false;
22182
+ }
22183
+ }
22184
+ function removeInstalledPackage(installDir, packageName) {
22185
+ const packageDir = join6(installDir, "node_modules", packageName);
22186
+ if (!existsSync4(packageDir))
22187
+ return false;
22188
+ rmSync(packageDir, { recursive: true, force: true });
22189
+ log(`[auto-update-checker] Package removed: ${packageDir}`);
22190
+ return true;
22191
+ }
22192
+ function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
22193
+ if (runtimePackageJsonPath) {
22194
+ const packageDir = dirname2(runtimePackageJsonPath);
22195
+ const nodeModulesDir = stripPackageNameFromPath(packageDir, PACKAGE_NAME);
22196
+ if (nodeModulesDir && basename(nodeModulesDir) === "node_modules") {
22197
+ const installDir = dirname2(nodeModulesDir);
22198
+ const packageJsonPath = join6(installDir, "package.json");
22199
+ if (existsSync4(packageJsonPath))
22200
+ return { installDir, packageJsonPath };
21958
22201
  }
21959
- } catch {}
21960
- return [];
22202
+ return null;
22203
+ }
22204
+ const legacyPackageJsonPath = join6(dirname2(CACHE_DIR), "package.json");
22205
+ if (existsSync4(legacyPackageJsonPath)) {
22206
+ return { installDir: dirname2(CACHE_DIR), packageJsonPath: legacyPackageJsonPath };
22207
+ }
22208
+ return null;
21961
22209
  }
21962
- async function sendIgnoredMessage(client, sessionId, text) {
22210
+ function preparePackageUpdate(version2, packageName = PACKAGE_NAME, runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
21963
22211
  try {
21964
- const c = client;
21965
- const promptInput = {
21966
- path: { id: sessionId },
21967
- body: {
21968
- noReply: true,
21969
- parts: [{ type: "text", text, ignored: true }]
21970
- }
21971
- };
21972
- if (typeof c.session?.prompt === "function") {
21973
- await Promise.resolve(c.session.prompt(promptInput));
21974
- return true;
22212
+ const installContext = resolveInstallContext(runtimePackageJsonPath);
22213
+ if (!installContext) {
22214
+ warn("[auto-update-checker] No install context found for auto-update");
22215
+ return null;
21975
22216
  }
21976
- if (typeof c.session?.promptAsync === "function") {
21977
- await c.session.promptAsync(promptInput);
21978
- return true;
22217
+ if (!ensureDependencyVersion(installContext.packageJsonPath, packageName, version2))
22218
+ return null;
22219
+ const packageRemoved = removeInstalledPackage(installContext.installDir, packageName);
22220
+ const lockRemoved = removeFromBunLock(installContext.installDir, packageName);
22221
+ if (!packageRemoved && !lockRemoved) {
22222
+ log(`[auto-update-checker] No cached package artifacts removed for ${packageName}; continuing with updated dependency spec`);
21979
22223
  }
22224
+ return installContext.installDir;
21980
22225
  } catch (err) {
21981
- log(`[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
22226
+ warn(`[auto-update-checker] Failed to prepare package update: ${String(err)}`);
22227
+ return null;
21982
22228
  }
21983
- return false;
21984
22229
  }
21985
- async function deleteMessage(serverUrl, sessionId, messageId) {
21986
- const auth = getServerAuth();
21987
- const url2 = `${serverUrl}/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`;
22230
+ async function runBunInstallSafe(installDir, options = {}) {
22231
+ let timeout = null;
21988
22232
  try {
21989
- const response = await fetch(url2, {
21990
- method: "DELETE",
21991
- headers: auth ? { Authorization: auth } : {},
21992
- signal: AbortSignal.timeout(1e4)
22233
+ if (options.signal?.aborted)
22234
+ return false;
22235
+ const proc = spawn("bun", ["install"], {
22236
+ cwd: installDir,
22237
+ stdio: "pipe"
21993
22238
  });
21994
- return response.ok;
21995
- } catch {
22239
+ const abortProcess = () => {
22240
+ try {
22241
+ proc.kill();
22242
+ } catch {}
22243
+ };
22244
+ options.signal?.addEventListener("abort", abortProcess, { once: true });
22245
+ const exitPromise = new Promise((resolveExit) => {
22246
+ proc.on("error", () => resolveExit(false));
22247
+ proc.on("exit", (code) => resolveExit(code === 0));
22248
+ });
22249
+ const timeoutPromise = new Promise((resolveTimeout) => {
22250
+ timeout = setTimeout(() => resolveTimeout("timeout"), options.timeoutMs ?? 60000);
22251
+ });
22252
+ const result = await Promise.race([exitPromise, timeoutPromise]);
22253
+ options.signal?.removeEventListener("abort", abortProcess);
22254
+ if (result === "timeout" || options.signal?.aborted) {
22255
+ abortProcess();
22256
+ return false;
22257
+ }
22258
+ return result;
22259
+ } catch (err) {
22260
+ warn(`[auto-update-checker] bun install error: ${String(err)}`);
22261
+ return false;
22262
+ } finally {
22263
+ if (timeout)
22264
+ clearTimeout(timeout);
22265
+ }
22266
+ }
22267
+
22268
+ // src/hooks/auto-update-checker/index.ts
22269
+ function createAutoUpdateCheckerHook(ctx, options = {}) {
22270
+ const {
22271
+ enabled = true,
22272
+ showStartupToast = true,
22273
+ autoUpdate = true,
22274
+ npmRegistryUrl = NPM_REGISTRY_URL,
22275
+ fetchTimeoutMs = NPM_FETCH_TIMEOUT,
22276
+ signal = new AbortController().signal
22277
+ } = options;
22278
+ let hasChecked = false;
22279
+ return async ({ event }) => {
22280
+ if (!enabled)
22281
+ return;
22282
+ if (event.type !== "session.created")
22283
+ return;
22284
+ if (hasChecked)
22285
+ return;
22286
+ if (getParentId(event.properties))
22287
+ return;
22288
+ hasChecked = true;
22289
+ setTimeout(() => {
22290
+ runStartupCheck(ctx, {
22291
+ showStartupToast,
22292
+ autoUpdate,
22293
+ npmRegistryUrl,
22294
+ fetchTimeoutMs,
22295
+ signal
22296
+ }).catch((err) => {
22297
+ warn(`[auto-update-checker] Background update check failed: ${String(err)}`);
22298
+ });
22299
+ }, 0);
22300
+ };
22301
+ }
22302
+ function getParentId(properties) {
22303
+ if (!properties || typeof properties !== "object" || Array.isArray(properties))
22304
+ return null;
22305
+ const info = properties.info;
22306
+ if (!info || typeof info !== "object" || Array.isArray(info))
22307
+ return null;
22308
+ const parentID = info.parentID;
22309
+ return typeof parentID === "string" && parentID.length > 0 ? parentID : null;
22310
+ }
22311
+ async function runStartupCheck(ctx, options) {
22312
+ if (options.signal.aborted)
22313
+ return;
22314
+ const cachedVersion = getCachedVersion();
22315
+ const localDevVersion = getLocalDevVersion(ctx.directory);
22316
+ const displayVersion = localDevVersion ?? cachedVersion;
22317
+ if (localDevVersion) {
22318
+ if (options.showStartupToast) {
22319
+ showToast(ctx, `AFT ${displayVersion} (dev)`, "Running in local development mode.", "info");
22320
+ }
22321
+ log("[auto-update-checker] Local development mode");
22322
+ return;
22323
+ }
22324
+ if (options.showStartupToast) {
22325
+ showToast(ctx, `AFT ${displayVersion ?? "unknown"}`, "@cortexkit/aft-opencode is active.", "info");
22326
+ }
22327
+ await runBackgroundUpdateCheck(ctx, options);
22328
+ }
22329
+ async function runBackgroundUpdateCheck(ctx, options) {
22330
+ if (options.signal.aborted)
22331
+ return;
22332
+ const pluginInfo = findPluginEntry(ctx.directory);
22333
+ if (!pluginInfo) {
22334
+ log("[auto-update-checker] Plugin not found in config");
22335
+ return;
22336
+ }
22337
+ const cachedVersion = getCachedVersion(pluginInfo.entry);
22338
+ const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion;
22339
+ if (!currentVersion) {
22340
+ log("[auto-update-checker] No version found (cached or pinned)");
22341
+ return;
22342
+ }
22343
+ const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
22344
+ const latestVersion = await getLatestVersion(channel, {
22345
+ registryUrl: options.npmRegistryUrl,
22346
+ timeoutMs: options.fetchTimeoutMs,
22347
+ signal: options.signal
22348
+ });
22349
+ if (!latestVersion) {
22350
+ warn(`[auto-update-checker] Failed to fetch latest version for channel: ${channel}`);
22351
+ showToast(ctx, "AFT update check failed", "Could not check npm for @cortexkit/aft-opencode updates. Continuing with the cached version.", "warning", 8000);
22352
+ return;
22353
+ }
22354
+ if (currentVersion === latestVersion) {
22355
+ log(`[auto-update-checker] Already on latest version for channel: ${channel}`);
22356
+ return;
22357
+ }
22358
+ log(`[auto-update-checker] Update available (${channel}): ${currentVersion} \u2192 ${latestVersion}`);
22359
+ if (pluginInfo.isPinned) {
22360
+ showToast(ctx, `AFT ${latestVersion}`, `v${latestVersion} available. Version is pinned; update your OpenCode plugin config to upgrade.`, "info", 8000);
22361
+ log("[auto-update-checker] Version is pinned; skipping auto-update");
22362
+ return;
22363
+ }
22364
+ if (!options.autoUpdate) {
22365
+ showToast(ctx, `AFT ${latestVersion}`, `v${latestVersion} available. Auto-update is disabled.`, "info", 8000);
22366
+ log("[auto-update-checker] Auto-update disabled, notification only");
22367
+ return;
22368
+ }
22369
+ const installDir = preparePackageUpdate(latestVersion, PACKAGE_NAME);
22370
+ if (!installDir) {
22371
+ showToast(ctx, `AFT ${latestVersion}`, `v${latestVersion} available. Auto-update could not prepare the active install.`, "warning", 8000);
22372
+ warn("[auto-update-checker] Failed to prepare install root for auto-update");
22373
+ return;
22374
+ }
22375
+ const installSuccess = await runBunInstallSafe(installDir, { signal: options.signal });
22376
+ if (installSuccess) {
22377
+ showToast(ctx, "AFT Updated!", `v${currentVersion} \u2192 v${latestVersion}
22378
+ Restart OpenCode to apply.`, "success", 8000);
22379
+ log(`[auto-update-checker] Update installed: ${currentVersion} \u2192 ${latestVersion}`);
22380
+ return;
22381
+ }
22382
+ showToast(ctx, `AFT ${latestVersion}`, `v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`, "error", 8000);
22383
+ warn("[auto-update-checker] bun install failed; update not installed");
22384
+ }
22385
+ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
22386
+ ctx.client.tui.showToast({ body: { title, message, variant, duration: duration3 } }).catch(() => {});
22387
+ }
22388
+
22389
+ // src/lsp-auto-install.ts
22390
+ import { spawn as spawn2 } from "child_process";
22391
+ import { createHash } from "crypto";
22392
+ import { createReadStream, statSync as statSync3 } from "fs";
22393
+
22394
+ // src/lsp-cache.ts
22395
+ import {
22396
+ closeSync,
22397
+ mkdirSync as mkdirSync2,
22398
+ openSync,
22399
+ readFileSync as readFileSync4,
22400
+ statSync as statSync2,
22401
+ unlinkSync as unlinkSync2,
22402
+ writeFileSync as writeFileSync3
22403
+ } from "fs";
22404
+ import { homedir as homedir5 } from "os";
22405
+ import { join as join7 } from "path";
22406
+ function aftCacheBase() {
22407
+ const override = process.env.AFT_CACHE_DIR;
22408
+ if (override && override.length > 0)
22409
+ return override;
22410
+ if (process.platform === "win32") {
22411
+ const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
22412
+ const base2 = localAppData || join7(homedir5(), "AppData", "Local");
22413
+ return join7(base2, "aft");
22414
+ }
22415
+ const base = process.env.XDG_CACHE_HOME || join7(homedir5(), ".cache");
22416
+ return join7(base, "aft");
22417
+ }
22418
+ function lspCacheRoot() {
22419
+ return join7(aftCacheBase(), "lsp-packages");
22420
+ }
22421
+ function lspPackageDir(npmPackage) {
22422
+ return join7(lspCacheRoot(), encodeURIComponent(npmPackage));
22423
+ }
22424
+ function lspBinaryPath(npmPackage, binary) {
22425
+ return join7(lspPackageDir(npmPackage), "node_modules", ".bin", binary);
22426
+ }
22427
+ function lspBinDir(npmPackage) {
22428
+ return join7(lspPackageDir(npmPackage), "node_modules", ".bin");
22429
+ }
22430
+ function isInstalled(npmPackage, binary) {
22431
+ for (const candidate of lspBinaryCandidates(binary)) {
22432
+ try {
22433
+ if (statSync2(join7(lspBinDir(npmPackage), candidate)).isFile())
22434
+ return true;
22435
+ } catch {}
22436
+ }
22437
+ return false;
22438
+ }
22439
+ function lspBinaryCandidates(binary) {
22440
+ if (process.platform !== "win32")
22441
+ return [binary];
22442
+ return [binary, `${binary}.cmd`, `${binary}.exe`, `${binary}.bat`];
22443
+ }
22444
+ var INSTALLED_META_FILE = ".aft-installed";
22445
+ function writeInstalledMetaIn(installDir, version2, sha256) {
22446
+ try {
22447
+ mkdirSync2(installDir, { recursive: true });
22448
+ const meta3 = {
22449
+ version: version2,
22450
+ installedAt: new Date().toISOString(),
22451
+ ...sha256 ? { sha256 } : {}
22452
+ };
22453
+ writeFileSync3(join7(installDir, INSTALLED_META_FILE), JSON.stringify(meta3), "utf8");
22454
+ } catch (err) {
22455
+ log(`[lsp-cache] failed to write installed-meta in ${installDir}: ${err}`);
22456
+ }
22457
+ }
22458
+ function readInstalledMetaIn(installDir) {
22459
+ const path2 = join7(installDir, INSTALLED_META_FILE);
22460
+ try {
22461
+ if (!statSync2(path2).isFile())
22462
+ return null;
22463
+ const raw = readFileSync4(path2, "utf8");
22464
+ const parsed = JSON.parse(raw);
22465
+ if (typeof parsed.version !== "string" || parsed.version.length === 0)
22466
+ return null;
22467
+ return {
22468
+ version: parsed.version,
22469
+ installedAt: typeof parsed.installedAt === "string" ? parsed.installedAt : "",
22470
+ ...typeof parsed.sha256 === "string" && parsed.sha256.length > 0 ? { sha256: parsed.sha256 } : {}
22471
+ };
22472
+ } catch {
22473
+ return null;
22474
+ }
22475
+ }
22476
+ function writeInstalledMeta(packageKey, version2, sha256) {
22477
+ writeInstalledMetaIn(lspPackageDir(packageKey), version2, sha256);
22478
+ }
22479
+ function readInstalledMeta(packageKey) {
22480
+ return readInstalledMetaIn(lspPackageDir(packageKey));
22481
+ }
22482
+ function lockPath(npmPackage) {
22483
+ return join7(lspPackageDir(npmPackage), ".aft-installing");
22484
+ }
22485
+ var STALE_LOCK_MS = 30 * 60 * 1000;
22486
+ function acquireInstallLock(lockKey) {
22487
+ mkdirSync2(lspPackageDir(lockKey), { recursive: true });
22488
+ const lock = lockPath(lockKey);
22489
+ const tryClaim = () => {
22490
+ try {
22491
+ const fd = openSync(lock, "wx");
22492
+ try {
22493
+ writeFileSync3(fd, `${process.pid}
22494
+ ${new Date().toISOString()}
22495
+ `);
22496
+ } finally {
22497
+ closeSync(fd);
22498
+ }
22499
+ return true;
22500
+ } catch (err) {
22501
+ const code = err.code;
22502
+ if (code === "EEXIST")
22503
+ return false;
22504
+ warn(`[lsp] unexpected error acquiring install lock for ${lockKey}: ${err}`);
22505
+ return false;
22506
+ }
22507
+ };
22508
+ if (tryClaim())
22509
+ return true;
22510
+ let owningPid = null;
22511
+ let lockMtimeMs = 0;
22512
+ try {
22513
+ const raw = readFileSync4(lock, "utf8");
22514
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
22515
+ const parsed = Number.parseInt(firstLine, 10);
22516
+ if (Number.isFinite(parsed) && parsed > 0)
22517
+ owningPid = parsed;
22518
+ lockMtimeMs = statSync2(lock).mtimeMs;
22519
+ } catch {
22520
+ return tryClaim();
22521
+ }
22522
+ const age = Date.now() - lockMtimeMs;
22523
+ const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS;
22524
+ const skipLiveness = process.platform === "win32";
22525
+ const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive(owningPid);
22526
+ if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
22527
+ return false;
22528
+ }
22529
+ log(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
22530
+ try {
22531
+ unlinkSync2(lock);
22532
+ } catch {}
22533
+ return tryClaim();
22534
+ }
22535
+ function isProcessAlive(pid) {
22536
+ try {
22537
+ process.kill(pid, 0);
22538
+ return true;
22539
+ } catch (err) {
22540
+ const code = err.code;
22541
+ if (code === "ESRCH")
22542
+ return false;
22543
+ return true;
22544
+ }
22545
+ }
22546
+ function releaseInstallLock(lockKey) {
22547
+ const lock = lockPath(lockKey);
22548
+ try {
22549
+ let owningPid = null;
22550
+ try {
22551
+ const raw = readFileSync4(lock, "utf8");
22552
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
22553
+ const parsed = Number.parseInt(firstLine, 10);
22554
+ if (Number.isFinite(parsed) && parsed > 0)
22555
+ owningPid = parsed;
22556
+ } catch (readErr) {
22557
+ const code = readErr.code;
22558
+ if (code === "ENOENT")
22559
+ return;
22560
+ warn(`[lsp] could not read install lock for ${lockKey} during release: ${readErr}`);
22561
+ return;
22562
+ }
22563
+ if (owningPid !== process.pid) {
22564
+ log(`[lsp] not releasing install lock for ${lockKey}: owned by pid ${owningPid ?? "unknown"} (we are ${process.pid})`);
22565
+ return;
22566
+ }
22567
+ try {
22568
+ unlinkSync2(lock);
22569
+ } catch (unlinkErr) {
22570
+ const code = unlinkErr.code;
22571
+ if (code !== "ENOENT") {
22572
+ warn(`[lsp] failed to release install lock for ${lockKey}: ${unlinkErr}`);
22573
+ }
22574
+ }
22575
+ } catch (err) {
22576
+ warn(`[lsp] unexpected error releasing install lock for ${lockKey}: ${err}`);
22577
+ }
22578
+ }
22579
+ async function withInstallLock(lockKey, task) {
22580
+ if (!acquireInstallLock(lockKey))
22581
+ return null;
22582
+ try {
22583
+ return await task();
22584
+ } finally {
22585
+ releaseInstallLock(lockKey);
22586
+ }
22587
+ }
22588
+ var VERSION_CHECK_FILE = ".aft-version-check";
22589
+ function readVersionCheck(npmPackage) {
22590
+ const file2 = join7(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
22591
+ try {
22592
+ const raw = readFileSync4(file2, "utf8");
22593
+ const parsed = JSON.parse(raw);
22594
+ if (typeof parsed.last_checked === "string") {
22595
+ return {
22596
+ last_checked: parsed.last_checked,
22597
+ latest_eligible: typeof parsed.latest_eligible === "string" ? parsed.latest_eligible : null
22598
+ };
22599
+ }
22600
+ return null;
22601
+ } catch {
22602
+ return null;
22603
+ }
22604
+ }
22605
+ function writeVersionCheck(npmPackage, latest) {
22606
+ mkdirSync2(lspPackageDir(npmPackage), { recursive: true });
22607
+ const file2 = join7(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
22608
+ const record2 = {
22609
+ last_checked: new Date().toISOString(),
22610
+ latest_eligible: latest
22611
+ };
22612
+ writeFileSync3(file2, JSON.stringify(record2, null, 2));
22613
+ }
22614
+ function shouldRecheckVersion(record2, weeklyCheckIntervalMs = 7 * 24 * 60 * 60 * 1000) {
22615
+ if (!record2)
22616
+ return true;
22617
+ const age = Date.now() - new Date(record2.last_checked).getTime();
22618
+ if (Number.isNaN(age) || age < 0)
22619
+ return true;
22620
+ return age >= weeklyCheckIntervalMs;
22621
+ }
22622
+
22623
+ // src/lsp-github-probe.ts
22624
+ function pickEligibleRelease(releases, graceDays, now = Date.now()) {
22625
+ const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
22626
+ const candidates = releases.filter((r) => !r.draft && !r.prerelease && typeof r.published_at === "string").map((r) => {
22627
+ const ts = Date.parse(r.published_at);
22628
+ return { release: r, ts };
22629
+ }).filter((c) => !Number.isNaN(c.ts)).sort((a, b) => b.ts - a.ts);
22630
+ const eligible = candidates.filter((c) => c.ts <= cutoff);
22631
+ const blockedByGrace = candidates.length > 0 && eligible.length === 0;
22632
+ const chosen = eligible[0]?.release;
22633
+ if (!chosen) {
22634
+ return { tag: null, assets: [], blockedByGrace };
22635
+ }
22636
+ return {
22637
+ tag: chosen.tag_name,
22638
+ assets: (chosen.assets ?? []).map((a) => ({
22639
+ name: a.name,
22640
+ url: a.browser_download_url,
22641
+ size: a.size
22642
+ })),
22643
+ blockedByGrace: false
22644
+ };
22645
+ }
22646
+ async function probeGithubReleases(githubRepo, graceDays, fetchImpl = fetch) {
22647
+ const url2 = `https://api.github.com/repos/${githubRepo}/releases?per_page=30`;
22648
+ try {
22649
+ const headers = { accept: "application/vnd.github+json" };
22650
+ if (process.env.GITHUB_TOKEN) {
22651
+ headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
22652
+ }
22653
+ const res = await fetchImpl(url2, {
22654
+ headers,
22655
+ signal: AbortSignal.timeout(1e4)
22656
+ });
22657
+ if (!res.ok) {
22658
+ warn(`[lsp] github releases probe failed for ${githubRepo}: HTTP ${res.status}`);
22659
+ return null;
22660
+ }
22661
+ const json2 = await res.json();
22662
+ if (!Array.isArray(json2)) {
22663
+ warn(`[lsp] unexpected response shape from github releases for ${githubRepo}`);
22664
+ return null;
22665
+ }
22666
+ return pickEligibleRelease(json2, graceDays);
22667
+ } catch (err) {
22668
+ warn(`[lsp] github releases probe failed for ${githubRepo}: ${err}`);
22669
+ return null;
22670
+ }
22671
+ }
22672
+ var SAFE_VERSION_RE = /^[A-Za-z0-9._+-]+$/;
22673
+ function assertSafeVersion(version2) {
22674
+ if (!SAFE_VERSION_RE.test(version2)) {
22675
+ throw new Error(`unsafe version/tag string ${JSON.stringify(version2)}: must match ${SAFE_VERSION_RE.source}`);
22676
+ }
22677
+ }
22678
+ function isSafeVersion(version2) {
22679
+ return typeof version2 === "string" && version2.length > 0 && SAFE_VERSION_RE.test(version2);
22680
+ }
22681
+ function stripTagV(tag) {
22682
+ assertSafeVersion(tag);
22683
+ return tag.startsWith("v") ? tag.slice(1) : tag;
22684
+ }
22685
+
22686
+ // src/lsp-npm-table.ts
22687
+ var NPM_LSP_TABLE = [
22688
+ {
22689
+ id: "typescript",
22690
+ npm: "typescript-language-server",
22691
+ binary: "typescript-language-server",
22692
+ extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
22693
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
22694
+ },
22695
+ {
22696
+ id: "python",
22697
+ npm: "pyright",
22698
+ binary: "pyright-langserver",
22699
+ extensions: ["py", "pyi"],
22700
+ rootMarkers: ["pyproject.toml", "pyrightconfig.json", "requirements.txt"]
22701
+ },
22702
+ {
22703
+ id: "yaml",
22704
+ npm: "yaml-language-server",
22705
+ binary: "yaml-language-server",
22706
+ extensions: ["yaml", "yml"]
22707
+ },
22708
+ {
22709
+ id: "bash",
22710
+ npm: "bash-language-server",
22711
+ binary: "bash-language-server",
22712
+ extensions: ["sh", "bash", "zsh"]
22713
+ },
22714
+ {
22715
+ id: "dockerfile",
22716
+ npm: "dockerfile-language-server-nodejs",
22717
+ binary: "docker-langserver",
22718
+ extensions: ["dockerfile"],
22719
+ rootMarkers: ["Dockerfile", "dockerfile"]
22720
+ },
22721
+ {
22722
+ id: "vue",
22723
+ npm: "@vue/language-server",
22724
+ binary: "vue-language-server",
22725
+ extensions: ["vue"]
22726
+ },
22727
+ {
22728
+ id: "astro",
22729
+ npm: "@astrojs/language-server",
22730
+ binary: "astro-ls",
22731
+ extensions: ["astro"]
22732
+ },
22733
+ {
22734
+ id: "svelte",
22735
+ npm: "svelte-language-server",
22736
+ binary: "svelteserver",
22737
+ extensions: ["svelte"]
22738
+ },
22739
+ {
22740
+ id: "php-intelephense",
22741
+ npm: "intelephense",
22742
+ binary: "intelephense",
22743
+ extensions: ["php"]
22744
+ },
22745
+ {
22746
+ id: "biome",
22747
+ npm: "@biomejs/biome",
22748
+ binary: "biome",
22749
+ extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts", "json", "jsonc"],
22750
+ rootMarkers: ["biome.json", "biome.jsonc"]
22751
+ }
22752
+ ];
22753
+
22754
+ // src/lsp-project-relevance.ts
22755
+ import { existsSync as existsSync5, readdirSync } from "fs";
22756
+ import { join as join8 } from "path";
22757
+ var MAX_WALK_DIRS = 200;
22758
+ var MAX_WALK_DEPTH = 4;
22759
+ var NOISE_DIRS = new Set([
22760
+ ".git",
22761
+ ".next",
22762
+ ".venv",
22763
+ "__pycache__",
22764
+ "build",
22765
+ "dist",
22766
+ "node_modules",
22767
+ "target"
22768
+ ]);
22769
+ function hasRootMarker(projectRoot, rootMarkers) {
22770
+ if (!rootMarkers)
22771
+ return false;
22772
+ for (const marker of rootMarkers) {
22773
+ if (existsSync5(join8(projectRoot, marker)))
22774
+ return true;
22775
+ }
22776
+ return false;
22777
+ }
22778
+ function relevantExtensionsInProject(projectRoot, extToServer) {
22779
+ const wanted = new Set(Object.keys(extToServer).map((ext) => ext.toLowerCase()));
22780
+ const found = new Set;
22781
+ if (wanted.size === 0)
22782
+ return found;
22783
+ const queue = [{ dir: projectRoot, depth: 0 }];
22784
+ let visitedDirs = 0;
22785
+ while (queue.length > 0 && visitedDirs < MAX_WALK_DIRS) {
22786
+ const current = queue.shift();
22787
+ if (!current)
22788
+ break;
22789
+ visitedDirs += 1;
22790
+ let entries;
22791
+ try {
22792
+ entries = readdirSync(current.dir, { withFileTypes: true });
22793
+ } catch {
22794
+ continue;
22795
+ }
22796
+ for (const entry of entries) {
22797
+ if (entry.isDirectory()) {
22798
+ if (current.depth < MAX_WALK_DEPTH && !NOISE_DIRS.has(entry.name.toLowerCase())) {
22799
+ queue.push({ dir: join8(current.dir, entry.name), depth: current.depth + 1 });
22800
+ }
22801
+ continue;
22802
+ }
22803
+ if (!entry.isFile())
22804
+ continue;
22805
+ const ext = extensionOf(entry.name);
22806
+ if (ext && wanted.has(ext))
22807
+ found.add(ext);
22808
+ }
22809
+ }
22810
+ return found;
22811
+ }
22812
+ function extensionOf(fileName) {
22813
+ const dot = fileName.lastIndexOf(".");
22814
+ if (dot < 0 || dot === fileName.length - 1)
22815
+ return null;
22816
+ return fileName.slice(dot + 1).toLowerCase();
22817
+ }
22818
+
22819
+ // src/lsp-registry-probe.ts
22820
+ var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
22821
+ function pickEligibleVersion(response, graceDays, now = Date.now()) {
22822
+ const times = response.time || {};
22823
+ const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
22824
+ const candidates = [];
22825
+ for (const [version2, publishedAt] of Object.entries(times)) {
22826
+ if (version2 === "created" || version2 === "modified")
22827
+ continue;
22828
+ if (version2.includes("-"))
22829
+ continue;
22830
+ if (typeof publishedAt !== "string")
22831
+ continue;
22832
+ const ts = Date.parse(publishedAt);
22833
+ if (Number.isNaN(ts))
22834
+ continue;
22835
+ candidates.push({ version: version2, publishedAt, ts });
22836
+ }
22837
+ candidates.sort((a, b) => b.ts - a.ts);
22838
+ const eligible = candidates.filter((c) => c.ts <= cutoff);
22839
+ const blockedByGrace = candidates.length > 0 && eligible.length === 0;
22840
+ return {
22841
+ version: eligible[0]?.version ?? null,
22842
+ blockedByGrace,
22843
+ eligible: eligible.map(({ version: version2, publishedAt }) => ({ version: version2, publishedAt }))
22844
+ };
22845
+ }
22846
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
22847
+ const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
22848
+ const url2 = `${NPM_REGISTRY_BASE}/${encoded}`;
22849
+ try {
22850
+ const res = await fetchImpl(url2, {
22851
+ headers: { accept: "application/json" },
22852
+ signal: AbortSignal.timeout(1e4)
22853
+ });
22854
+ if (!res.ok) {
22855
+ warn(`[lsp] registry probe failed for ${npmPackage}: HTTP ${res.status}`);
22856
+ return null;
22857
+ }
22858
+ const json2 = await res.json();
22859
+ return pickEligibleVersion(json2, graceDays);
22860
+ } catch (err) {
22861
+ warn(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
22862
+ return null;
22863
+ }
22864
+ }
22865
+
22866
+ // src/lsp-auto-install.ts
22867
+ function isProjectRelevant(spec, projectRoot, projectExtensions) {
22868
+ if (hasRootMarker(projectRoot, spec.rootMarkers))
22869
+ return true;
22870
+ const extensions = projectExtensions();
22871
+ return spec.extensions.some((ext) => extensions.has(ext.toLowerCase()));
22872
+ }
22873
+ var npmExtToServerIds = buildExtensionMap(NPM_LSP_TABLE);
22874
+ function buildExtensionMap(specs) {
22875
+ const byExt = {};
22876
+ for (const spec of specs) {
22877
+ for (const ext of spec.extensions) {
22878
+ const key = ext.toLowerCase();
22879
+ byExt[key] ??= [];
22880
+ byExt[key].push(spec.id);
22881
+ }
22882
+ }
22883
+ return byExt;
22884
+ }
22885
+ var inFlightAutoInstalls = new Set;
22886
+ function trackInFlightAutoInstall(controller, promise2) {
22887
+ const entry = { controller, promise: promise2 };
22888
+ inFlightAutoInstalls.add(entry);
22889
+ promise2.then(() => inFlightAutoInstalls.delete(entry), () => inFlightAutoInstalls.delete(entry));
22890
+ return promise2;
22891
+ }
22892
+ async function abortInFlightAutoInstalls() {
22893
+ const installs = Array.from(inFlightAutoInstalls);
22894
+ for (const install of installs) {
22895
+ install.controller.abort();
22896
+ }
22897
+ await Promise.allSettled(installs.map((install) => install.promise));
22898
+ }
22899
+ async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
22900
+ const pinned = config2.versions[spec.npm];
22901
+ if (pinned) {
22902
+ assertSafeVersion(pinned);
22903
+ return { version: pinned, pinned: true, probe: null };
22904
+ }
22905
+ const cached2 = readVersionCheck(spec.npm);
22906
+ const weeklyMs = config2.graceDays * 24 * 60 * 60 * 1000;
22907
+ const cachedSafe = isSafeVersion(cached2?.latest_eligible ?? null);
22908
+ if (cached2 && !shouldRecheckVersion(cached2, weeklyMs) && cachedSafe) {
22909
+ return { version: cached2.latest_eligible, pinned: false, probe: null };
22910
+ }
22911
+ const probe = await probeRegistry(spec.npm, config2.graceDays, fetchImpl);
22912
+ if (!probe) {
22913
+ return {
22914
+ version: cachedSafe ? cached2?.latest_eligible ?? null : null,
22915
+ pinned: false,
22916
+ probe: null
22917
+ };
22918
+ }
22919
+ writeVersionCheck(spec.npm, probe.version);
22920
+ return { version: probe.version, pinned: false, probe };
22921
+ }
22922
+ function runInstall(spec, version2, cwd, signal) {
22923
+ return new Promise((resolve2) => {
22924
+ const target = `${spec.npm}@${version2}`;
22925
+ log(`[lsp] installing ${target} to ${cwd}`);
22926
+ if (signal?.aborted) {
22927
+ warn(`[lsp] install ${target} aborted before spawn`);
22928
+ resolve2(false);
22929
+ return;
22930
+ }
22931
+ const child = spawn2("bun", ["add", target, "--cwd", cwd, "--ignore-scripts", "--silent"], {
22932
+ stdio: ["ignore", "pipe", "pipe"]
22933
+ });
22934
+ child.unref();
22935
+ let stderrBuf = "";
22936
+ let settled = false;
22937
+ let killTimer = null;
22938
+ const cleanup = () => {
22939
+ signal?.removeEventListener("abort", onAbort);
22940
+ if (killTimer)
22941
+ clearTimeout(killTimer);
22942
+ };
22943
+ const finish = (ok) => {
22944
+ if (settled)
22945
+ return;
22946
+ settled = true;
22947
+ cleanup();
22948
+ resolve2(ok);
22949
+ };
22950
+ const onAbort = () => {
22951
+ warn(`[lsp] install ${target} aborted during shutdown`);
22952
+ child.kill("SIGTERM");
22953
+ killTimer = setTimeout(() => {
22954
+ if (!settled)
22955
+ child.kill("SIGKILL");
22956
+ }, 5000);
22957
+ killTimer.unref?.();
22958
+ };
22959
+ signal?.addEventListener("abort", onAbort, { once: true });
22960
+ if (signal?.aborted)
22961
+ onAbort();
22962
+ child.stdout?.on("data", () => {});
22963
+ child.stderr?.on("data", (chunk) => {
22964
+ const text = String(chunk);
22965
+ stderrBuf += text;
22966
+ if (stderrBuf.length > 4096) {
22967
+ stderrBuf = stderrBuf.slice(stderrBuf.length - 4096);
22968
+ }
22969
+ });
22970
+ child.on("error", (err) => {
22971
+ error48(`[lsp] install ${target} failed to spawn: ${err}`);
22972
+ finish(false);
22973
+ });
22974
+ child.on("exit", (code) => {
22975
+ if (code === 0) {
22976
+ log(`[lsp] installed ${target}`);
22977
+ finish(true);
22978
+ } else {
22979
+ error48(`[lsp] install ${target} exited with code ${code}; last stderr:
22980
+ ${stderrBuf.trim()}`);
22981
+ finish(false);
22982
+ }
22983
+ });
22984
+ });
22985
+ }
22986
+ async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
22987
+ const outcome = await withInstallLock(spec.npm, async () => {
22988
+ const { version: version2, probe } = await resolveTargetVersion(spec, config2, fetchImpl);
22989
+ if (!version2) {
22990
+ const installed = isInstalled(spec.npm, spec.binary);
22991
+ if (installed) {
22992
+ warn(`[lsp] no eligible version of ${spec.npm} (grace=${config2.graceDays}d); keeping existing install`);
22993
+ return { started: false, reason: "kept existing install" };
22994
+ }
22995
+ const blocked = probe?.blockedByGrace ? `all versions are within ${config2.graceDays}-day grace window` : "registry probe failed";
22996
+ warn(`[lsp] skipping ${spec.npm}: ${blocked}`);
22997
+ return { started: false, reason: blocked };
22998
+ }
22999
+ if (isInstalled(spec.npm, spec.binary)) {
23000
+ const installedMeta = readInstalledMeta(spec.npm);
23001
+ if (installedMeta && installedMeta.version === version2) {
23002
+ if (installedMeta.sha256) {
23003
+ const currentHash = await hashInstalledBinary(spec).catch((err) => {
23004
+ warn(`[lsp] could not hash existing ${spec.npm} binary for TOFU check: ${err}`);
23005
+ return null;
23006
+ });
23007
+ if (currentHash && currentHash !== installedMeta.sha256) {
23008
+ error48(`[lsp] ${spec.npm}@${version2}: TOFU sha256 mismatch \u2014 refusing to use ` + `tampered binary. Recorded ${installedMeta.sha256}, current ${currentHash}. ` + `Run \`aft doctor --clear\` to re-install from scratch.`);
23009
+ return {
23010
+ started: false,
23011
+ reason: `TOFU sha256 mismatch on ${spec.npm}@${version2} \u2014 see plugin log`
23012
+ };
23013
+ }
23014
+ }
23015
+ return { started: false, reason: "already installed" };
23016
+ }
23017
+ if (installedMeta) {
23018
+ log(`[lsp] reinstalling ${spec.npm}: cached ${installedMeta.version} \u2260 target ${version2}`);
23019
+ } else {
23020
+ log(`[lsp] reinstalling ${spec.npm}@${version2}: no installed-version metadata recorded`);
23021
+ }
23022
+ }
23023
+ const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), signal).catch((err) => {
23024
+ error48(`[lsp] background install ${spec.npm} crashed: ${err}`);
23025
+ return false;
23026
+ });
23027
+ if (!ok) {
23028
+ return { started: true, reason: "install failed (see plugin log)" };
23029
+ }
23030
+ const installedHash = await hashInstalledBinary(spec).catch((err) => {
23031
+ warn(`[lsp] could not hash newly-installed ${spec.npm} binary: ${err}`);
23032
+ return null;
23033
+ });
23034
+ if (installedHash) {
23035
+ log(`[lsp] ${spec.npm}@${version2} installed sha256=${installedHash}`);
23036
+ }
23037
+ writeInstalledMeta(spec.npm, version2, installedHash ?? undefined);
23038
+ return { started: true };
23039
+ });
23040
+ if (outcome === null) {
23041
+ return { started: false, reason: "another install in progress" };
23042
+ }
23043
+ return outcome;
23044
+ }
23045
+ function cachedPackageDir(npmPackage) {
23046
+ return lspBinDir(npmPackage).replace(/[\\/]node_modules[\\/]\.bin[\\/]?$/, "");
23047
+ }
23048
+ function hashInstalledBinary(spec) {
23049
+ return new Promise((resolve2, reject) => {
23050
+ const candidates = process.platform === "win32" ? [
23051
+ lspBinaryPath(spec.npm, spec.binary),
23052
+ lspBinaryPath(spec.npm, `${spec.binary}.cmd`),
23053
+ lspBinaryPath(spec.npm, `${spec.binary}.exe`),
23054
+ lspBinaryPath(spec.npm, `${spec.binary}.bat`)
23055
+ ] : [lspBinaryPath(spec.npm, spec.binary)];
23056
+ let pathToHash = null;
23057
+ for (const p of candidates) {
23058
+ try {
23059
+ if (statSync3(p).isFile()) {
23060
+ pathToHash = p;
23061
+ break;
23062
+ }
23063
+ } catch {}
23064
+ }
23065
+ if (!pathToHash) {
23066
+ reject(new Error(`installed binary not found at any of: ${candidates.join(", ")}`));
23067
+ return;
23068
+ }
23069
+ const hash2 = createHash("sha256");
23070
+ const stream = createReadStream(pathToHash);
23071
+ stream.on("error", reject);
23072
+ stream.on("data", (chunk) => hash2.update(chunk));
23073
+ stream.on("end", () => resolve2(hash2.digest("hex")));
23074
+ });
23075
+ }
23076
+ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
23077
+ const cachedBinDirs = [];
23078
+ const skipped = [];
23079
+ const installPromises = [];
23080
+ let installsStarted = 0;
23081
+ let projectExtensions = null;
23082
+ const getProjectExtensions = () => {
23083
+ projectExtensions ??= relevantExtensionsInProject(projectRoot, npmExtToServerIds);
23084
+ return projectExtensions;
23085
+ };
23086
+ for (const spec of NPM_LSP_TABLE) {
23087
+ if (isInstalled(spec.npm, spec.binary)) {
23088
+ cachedBinDirs.push(lspBinDir(spec.npm));
23089
+ }
23090
+ if (config2.disabled.has(spec.id)) {
23091
+ skipped.push({ id: spec.id, reason: "disabled by config" });
23092
+ continue;
23093
+ }
23094
+ if (!config2.autoInstall) {
23095
+ skipped.push({ id: spec.id, reason: "auto_install: false" });
23096
+ continue;
23097
+ }
23098
+ if (!isProjectRelevant(spec, projectRoot, getProjectExtensions)) {
23099
+ skipped.push({ id: spec.id, reason: "not relevant to project" });
23100
+ continue;
23101
+ }
23102
+ installsStarted += 1;
23103
+ const controller = new AbortController;
23104
+ const promise2 = ensureServerInstalled(spec, config2, fetchImpl, controller.signal).then((outcome) => {
23105
+ if (!outcome.started)
23106
+ installsStarted -= 1;
23107
+ if (outcome.reason && outcome.reason !== "already installed") {
23108
+ skipped.push({ id: spec.id, reason: outcome.reason });
23109
+ }
23110
+ }, (err) => {
23111
+ installsStarted -= 1;
23112
+ const reason = err instanceof Error ? err.message : String(err);
23113
+ skipped.push({ id: spec.id, reason: `install error: ${reason}` });
23114
+ error48(`[lsp] background install ${spec.npm} promise rejected: ${reason}`);
23115
+ });
23116
+ installPromises.push(trackInFlightAutoInstall(controller, promise2));
23117
+ }
23118
+ return {
23119
+ cachedBinDirs,
23120
+ get installsStarted() {
23121
+ return installsStarted;
23122
+ },
23123
+ skipped,
23124
+ installsComplete: Promise.all(installPromises).then(() => {})
23125
+ };
23126
+ }
23127
+
23128
+ // src/lsp-github-install.ts
23129
+ import { execFileSync } from "child_process";
23130
+ import { createHash as createHash2, randomBytes } from "crypto";
23131
+ import {
23132
+ copyFileSync,
23133
+ createReadStream as createReadStream2,
23134
+ createWriteStream,
23135
+ existsSync as existsSync6,
23136
+ lstatSync,
23137
+ mkdirSync as mkdirSync3,
23138
+ readdirSync as readdirSync2,
23139
+ readlinkSync,
23140
+ realpathSync,
23141
+ renameSync,
23142
+ rmSync as rmSync2,
23143
+ statSync as statSync4,
23144
+ unlinkSync as unlinkSync3
23145
+ } from "fs";
23146
+ import { dirname as dirname3, join as join9, relative, resolve as resolve2 } from "path";
23147
+ import { Readable } from "stream";
23148
+ import { pipeline } from "stream/promises";
23149
+
23150
+ // src/lsp-github-table.ts
23151
+ function exe(platform2, name) {
23152
+ return platform2 === "win32" ? `${name}.exe` : name;
23153
+ }
23154
+ var CLANGD = {
23155
+ id: "clangd",
23156
+ githubRepo: "clangd/clangd",
23157
+ binary: "clangd",
23158
+ resolveAsset: (platform2, _arch, version2) => {
23159
+ const platformName = platform2 === "darwin" ? "mac" : platform2 === "linux" ? "linux" : "windows";
23160
+ return { name: `clangd-${platformName}-${version2}.zip`, archive: "zip" };
23161
+ },
23162
+ binaryPathInArchive: (platform2, _arch, version2) => `clangd_${version2}/bin/${exe(platform2, "clangd")}`
23163
+ };
23164
+ var LUA_LS = {
23165
+ id: "lua-ls",
23166
+ githubRepo: "LuaLS/lua-language-server",
23167
+ binary: "lua-language-server",
23168
+ resolveAsset: (platform2, arch, version2) => {
23169
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
23170
+ const platformName = platform2 === "darwin" ? "darwin" : platform2 === "linux" ? "linux" : "win32";
23171
+ const archName = arch === "arm64" ? "arm64" : "x64";
23172
+ return {
23173
+ name: `lua-language-server-${version2}-${platformName}-${archName}.${ext}`,
23174
+ archive: ext
23175
+ };
23176
+ },
23177
+ binaryPathInArchive: (platform2, _arch, _version) => `bin/${exe(platform2, "lua-language-server")}`
23178
+ };
23179
+ var ZLS = {
23180
+ id: "zls",
23181
+ githubRepo: "zigtools/zls",
23182
+ binary: "zls",
23183
+ resolveAsset: (platform2, arch, _version) => {
23184
+ const ext = platform2 === "win32" ? "zip" : "tar.xz";
23185
+ const archName = arch === "arm64" ? "aarch64" : "x86_64";
23186
+ const platformName = platform2 === "darwin" ? "macos" : platform2 === "linux" ? "linux" : "windows";
23187
+ return { name: `zls-${archName}-${platformName}.${ext}`, archive: ext };
23188
+ },
23189
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "zls")
23190
+ };
23191
+ var TINYMIST = {
23192
+ id: "tinymist",
23193
+ githubRepo: "Myriad-Dreamin/tinymist",
23194
+ binary: "tinymist",
23195
+ resolveAsset: (platform2, arch, _version) => {
23196
+ const archName = arch === "arm64" ? "aarch64" : "x86_64";
23197
+ const triple = platform2 === "darwin" ? "apple-darwin" : platform2 === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
23198
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
23199
+ return { name: `tinymist-${archName}-${triple}.${ext}`, archive: ext };
23200
+ },
23201
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "tinymist")
23202
+ };
23203
+ var TEXLAB = {
23204
+ id: "texlab",
23205
+ githubRepo: "latex-lsp/texlab",
23206
+ binary: "texlab",
23207
+ resolveAsset: (platform2, arch, _version) => {
23208
+ const archName = arch === "arm64" ? "aarch64" : "x86_64";
23209
+ const platformName = platform2 === "darwin" ? "macos" : platform2 === "linux" ? "linux" : "windows";
23210
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
23211
+ return { name: `texlab-${archName}-${platformName}.${ext}`, archive: ext };
23212
+ },
23213
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "texlab")
23214
+ };
23215
+ var GITHUB_LSP_TABLE = [
23216
+ CLANGD,
23217
+ LUA_LS,
23218
+ ZLS,
23219
+ TINYMIST,
23220
+ TEXLAB
23221
+ ];
23222
+ function detectHostPlatform() {
23223
+ const platform2 = process.platform;
23224
+ if (platform2 !== "darwin" && platform2 !== "linux" && platform2 !== "win32")
23225
+ return null;
23226
+ const arch = process.arch;
23227
+ if (arch === "x64")
23228
+ return { platform: platform2, arch: "x64" };
23229
+ if (arch === "arm64")
23230
+ return { platform: platform2, arch: "arm64" };
23231
+ return null;
23232
+ }
23233
+
23234
+ // src/lsp-github-install.ts
23235
+ function ghCacheRoot() {
23236
+ return join9(aftCacheBase(), "lsp-binaries");
23237
+ }
23238
+ function ghPackageDir(spec) {
23239
+ return join9(ghCacheRoot(), spec.id);
23240
+ }
23241
+ function ghBinDir(spec) {
23242
+ return join9(ghPackageDir(spec), "bin");
23243
+ }
23244
+ function ghExtractDir(spec) {
23245
+ return join9(ghPackageDir(spec), "extracted");
23246
+ }
23247
+ function ghBinaryPath(spec, platform2) {
23248
+ const ext = platform2 === "win32" ? ".exe" : "";
23249
+ return join9(ghBinDir(spec), `${spec.binary}${ext}`);
23250
+ }
23251
+ function isGithubInstalled(spec, platform2) {
23252
+ for (const candidate of ghBinaryCandidates(spec, platform2)) {
23253
+ try {
23254
+ if (statSync4(join9(ghBinDir(spec), candidate)).isFile())
23255
+ return true;
23256
+ } catch {}
23257
+ }
23258
+ return false;
23259
+ }
23260
+ function ghBinaryCandidates(spec, platform2) {
23261
+ if (platform2 !== "win32")
23262
+ return [spec.binary];
23263
+ return [spec.binary, `${spec.binary}.cmd`, `${spec.binary}.exe`, `${spec.binary}.bat`];
23264
+ }
23265
+ var MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
23266
+ var MAX_EXTRACT_BYTES = 1024 * 1024 * 1024;
23267
+ function sha256OfFile(path2) {
23268
+ return new Promise((resolve3, reject) => {
23269
+ const hash2 = createHash2("sha256");
23270
+ const stream = createReadStream2(path2);
23271
+ stream.on("error", reject);
23272
+ stream.on("data", (chunk) => hash2.update(chunk));
23273
+ stream.on("end", () => resolve3(hash2.digest("hex")));
23274
+ });
23275
+ }
23276
+ async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
23277
+ const candidates = [];
23278
+ candidates.push(tag);
23279
+ if (!tag.startsWith("v")) {
23280
+ candidates.push(`v${tag}`);
23281
+ } else {
23282
+ candidates.push(tag.slice(1));
23283
+ }
23284
+ const headers = {
23285
+ accept: "application/vnd.github+json",
23286
+ "user-agent": "aft-opencode",
23287
+ "x-github-api-version": "2022-11-28"
23288
+ };
23289
+ if (process.env.GITHUB_TOKEN) {
23290
+ headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
23291
+ }
23292
+ for (const candidate of candidates) {
23293
+ const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
23294
+ const timeout = controlledTimeoutSignal(15000, signal);
23295
+ try {
23296
+ const res = await fetchImpl(url2, {
23297
+ headers,
23298
+ redirect: "follow",
23299
+ signal: timeout.signal
23300
+ });
23301
+ if (res.status === 404)
23302
+ continue;
23303
+ if (!res.ok) {
23304
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: HTTP ${res.status}`);
23305
+ return null;
23306
+ }
23307
+ const json2 = await res.json();
23308
+ if (!json2.tag_name || !Array.isArray(json2.assets)) {
23309
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: malformed response`);
23310
+ return null;
23311
+ }
23312
+ const assets = json2.assets.filter((a) => typeof a.name === "string" && typeof a.browser_download_url === "string").map((a) => ({
23313
+ name: a.name,
23314
+ url: a.browser_download_url,
23315
+ size: typeof a.size === "number" ? a.size : undefined
23316
+ }));
23317
+ return { tag: json2.tag_name, assets };
23318
+ } catch (err) {
23319
+ if (signal?.aborted) {
23320
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: aborted`);
23321
+ return null;
23322
+ }
23323
+ warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: ${err}`);
23324
+ } finally {
23325
+ timeout.cleanup();
23326
+ }
23327
+ }
23328
+ return null;
23329
+ }
23330
+ async function resolveTargetTag(spec, config2, fetchImpl, signal) {
23331
+ const pinned = config2.versions[spec.githubRepo];
23332
+ if (pinned) {
23333
+ try {
23334
+ assertSafeVersion(pinned);
23335
+ } catch (err) {
23336
+ return {
23337
+ tag: null,
23338
+ assets: [],
23339
+ blockedByGrace: false,
23340
+ reason: `invalid pinned version ${JSON.stringify(pinned)}: ${err instanceof Error ? err.message : String(err)}`
23341
+ };
23342
+ }
23343
+ const release = await fetchReleaseByTag(spec.githubRepo, pinned, fetchImpl, signal);
23344
+ if (release) {
23345
+ return {
23346
+ tag: release.tag,
23347
+ assets: release.assets,
23348
+ blockedByGrace: false
23349
+ };
23350
+ }
23351
+ return {
23352
+ tag: null,
23353
+ assets: [],
23354
+ blockedByGrace: false,
23355
+ reason: `pinned tag ${pinned} not found on GitHub`
23356
+ };
23357
+ }
23358
+ const cached2 = readVersionCheck(spec.githubRepo);
23359
+ const weeklyMs = config2.graceDays * 24 * 60 * 60 * 1000;
23360
+ const cachedTag = cached2?.latest_eligible ?? null;
23361
+ const cachedSafe = isSafeVersion(cachedTag);
23362
+ if (cached2 && !shouldRecheckVersion(cached2, weeklyMs) && cachedSafe) {
23363
+ const release = await fetchReleaseByTag(spec.githubRepo, cachedTag, fetchImpl);
23364
+ if (release) {
23365
+ return {
23366
+ tag: release.tag,
23367
+ assets: release.assets,
23368
+ blockedByGrace: false
23369
+ };
23370
+ }
23371
+ }
23372
+ const probe = await probeGithubReleases(spec.githubRepo, config2.graceDays, fetchImpl);
23373
+ if (!probe) {
23374
+ return {
23375
+ tag: null,
23376
+ assets: [],
23377
+ blockedByGrace: false,
23378
+ reason: "github releases probe failed"
23379
+ };
23380
+ }
23381
+ writeVersionCheck(spec.githubRepo, probe.tag);
23382
+ return { tag: probe.tag, assets: probe.assets, blockedByGrace: probe.blockedByGrace };
23383
+ }
23384
+ function controlledTimeoutSignal(timeoutMs, parent) {
23385
+ const controller = new AbortController;
23386
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
23387
+ timeout.unref?.();
23388
+ const abort = () => controller.abort();
23389
+ parent?.addEventListener("abort", abort, { once: true });
23390
+ if (parent?.aborted)
23391
+ abort();
23392
+ return {
23393
+ signal: controller.signal,
23394
+ cleanup: () => {
23395
+ clearTimeout(timeout);
23396
+ parent?.removeEventListener("abort", abort);
23397
+ }
23398
+ };
23399
+ }
23400
+ var ALLOWED_DOWNLOAD_HOSTS = new Set([
23401
+ "github.com",
23402
+ "api.github.com",
23403
+ "objects.githubusercontent.com",
23404
+ "release-assets.githubusercontent.com",
23405
+ "raw.githubusercontent.com",
23406
+ "codeload.github.com"
23407
+ ]);
23408
+ function assertAllowedDownloadUrl(rawUrl) {
23409
+ let parsed;
23410
+ try {
23411
+ parsed = new URL(rawUrl);
23412
+ } catch {
23413
+ throw new Error(`download url is not a valid URL: ${rawUrl}`);
23414
+ }
23415
+ if (parsed.protocol !== "https:") {
23416
+ throw new Error(`download url must be https (got ${parsed.protocol}): ${rawUrl}`);
23417
+ }
23418
+ if (!ALLOWED_DOWNLOAD_HOSTS.has(parsed.hostname.toLowerCase())) {
23419
+ throw new Error(`download url host ${parsed.hostname} is not in the GitHub allowlist: ${rawUrl}`);
23420
+ }
23421
+ return parsed;
23422
+ }
23423
+ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
23424
+ assertAllowedDownloadUrl(url2);
23425
+ if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES) {
23426
+ throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES} (set lsp.versions to pin a smaller release if this is wrong)`);
23427
+ }
23428
+ const timeout = controlledTimeoutSignal(120000, signal);
23429
+ try {
23430
+ const res = await fetchImpl(url2, {
23431
+ headers: { accept: "application/octet-stream" },
23432
+ redirect: "follow",
23433
+ signal: timeout.signal
23434
+ });
23435
+ if (!res.ok || !res.body) {
23436
+ throw new Error(`download failed (${res.status})`);
23437
+ }
23438
+ const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
23439
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES) {
23440
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES}`);
23441
+ }
23442
+ mkdirSync3(dirname3(destPath), { recursive: true });
23443
+ let bytesWritten = 0;
23444
+ const guard = new TransformStream({
23445
+ transform(chunk, controller) {
23446
+ bytesWritten += chunk.byteLength;
23447
+ if (bytesWritten > MAX_DOWNLOAD_BYTES) {
23448
+ controller.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES} bytes after streaming (server lied about size or sent unbounded body)`));
23449
+ return;
23450
+ }
23451
+ controller.enqueue(chunk);
23452
+ }
23453
+ });
23454
+ const guarded = res.body.pipeThrough(guard);
23455
+ const nodeStream = Readable.fromWeb(guarded);
23456
+ await pipeline(nodeStream, createWriteStream(destPath), { signal: timeout.signal });
23457
+ } catch (err) {
23458
+ try {
23459
+ unlinkSync3(destPath);
23460
+ } catch {}
23461
+ throw err;
23462
+ } finally {
23463
+ timeout.cleanup();
23464
+ }
23465
+ }
23466
+ function validateExtraction(stagingRoot) {
23467
+ const realStagingRoot = realpathSync(stagingRoot);
23468
+ let totalBytes = 0;
23469
+ const walk = (dir) => {
23470
+ let entries;
23471
+ try {
23472
+ entries = readdirSync2(dir);
23473
+ } catch (err) {
23474
+ throw new Error(`failed to read staging dir ${dir}: ${err}`);
23475
+ }
23476
+ for (const entry of entries) {
23477
+ const full = join9(dir, entry);
23478
+ let lst;
23479
+ try {
23480
+ lst = lstatSync(full);
23481
+ } catch (err) {
23482
+ throw new Error(`failed to lstat ${full}: ${err}`);
23483
+ }
23484
+ if (lst.isSymbolicLink()) {
23485
+ let target = "<unreadable>";
23486
+ try {
23487
+ target = readlinkSync(full);
23488
+ } catch {}
23489
+ throw new Error(`archive contains symlink ${relative(realStagingRoot, full)} \u2192 ${target}; rejecting (zip-slip defense)`);
23490
+ }
23491
+ let realFull;
23492
+ try {
23493
+ realFull = realpathSync(full);
23494
+ } catch (err) {
23495
+ throw new Error(`failed to realpath ${full}: ${err}`);
23496
+ }
23497
+ const rel = relative(realStagingRoot, realFull);
23498
+ if (rel.startsWith("..") || resolve2(realStagingRoot, rel) !== realFull) {
23499
+ throw new Error(`archive entry escapes staging root: ${full} \u2192 ${realFull} (zip-slip defense)`);
23500
+ }
23501
+ if (lst.isDirectory()) {
23502
+ walk(full);
23503
+ } else if (lst.isFile()) {
23504
+ totalBytes += lst.size;
23505
+ if (totalBytes > MAX_EXTRACT_BYTES) {
23506
+ throw new Error(`extracted archive exceeds ${MAX_EXTRACT_BYTES} bytes (decompression bomb defense): saw ${totalBytes} bytes before hitting the cap`);
23507
+ }
23508
+ } else {
23509
+ throw new Error(`archive contains non-file/non-dir entry: ${full}`);
23510
+ }
23511
+ }
23512
+ };
23513
+ walk(realStagingRoot);
23514
+ }
23515
+ function extractArchiveSafely(archivePath, destDir, archiveType) {
23516
+ const suffix = randomBytes(8).toString("hex");
23517
+ const stagingDir = `${destDir}.staging-${suffix}`;
23518
+ try {
23519
+ rmSync2(stagingDir, { recursive: true, force: true });
23520
+ } catch {}
23521
+ mkdirSync3(stagingDir, { recursive: true });
23522
+ try {
23523
+ runPlatformExtractor(archivePath, stagingDir, archiveType);
23524
+ validateExtraction(stagingDir);
23525
+ try {
23526
+ rmSync2(destDir, { recursive: true, force: true });
23527
+ } catch {}
23528
+ renameSync(stagingDir, destDir);
23529
+ } catch (err) {
23530
+ try {
23531
+ rmSync2(stagingDir, { recursive: true, force: true });
23532
+ } catch {}
23533
+ throw err;
23534
+ }
23535
+ }
23536
+ function runPlatformExtractor(archivePath, destDir, archiveType) {
23537
+ if (archiveType === "zip") {
23538
+ if (process.platform === "win32") {
23539
+ execFileSync("tar.exe", ["-xf", archivePath, "-C", destDir], {
23540
+ stdio: "pipe",
23541
+ timeout: 180000
23542
+ });
23543
+ return;
23544
+ }
23545
+ execFileSync("unzip", ["-q", "-o", archivePath, "-d", destDir], {
23546
+ stdio: "pipe",
23547
+ timeout: 180000
23548
+ });
23549
+ return;
23550
+ }
23551
+ if (archiveType === "tar.gz") {
23552
+ execFileSync("tar", ["-xzf", archivePath, "-C", destDir], {
23553
+ stdio: "pipe",
23554
+ timeout: 180000
23555
+ });
23556
+ return;
23557
+ }
23558
+ if (archiveType === "tar.xz") {
23559
+ execFileSync("tar", ["-xf", archivePath, "-C", destDir], {
23560
+ stdio: "pipe",
23561
+ timeout: 180000
23562
+ });
23563
+ return;
23564
+ }
23565
+ throw new Error(`unsupported archive type: ${archiveType}`);
23566
+ }
23567
+ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal) {
23568
+ const version2 = stripTagV(tag);
23569
+ const expected = spec.resolveAsset(platform2, arch, version2);
23570
+ if (!expected) {
23571
+ warn(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform2}/${arch}`);
23572
+ return null;
23573
+ }
23574
+ const matchingAsset = assets.find((a) => a.name === expected.name);
23575
+ if (!matchingAsset) {
23576
+ warn(`[lsp] ${spec.id}: asset ${expected.name} not found in release ${tag} (${assets.length} assets available)`);
23577
+ return null;
23578
+ }
23579
+ const pkgDir = ghPackageDir(spec);
23580
+ const extractDir = ghExtractDir(spec);
23581
+ const archivePath = join9(pkgDir, expected.name);
23582
+ log(`[lsp] downloading ${spec.id} ${tag} \u2192 ${matchingAsset.url}`);
23583
+ try {
23584
+ await downloadFile(matchingAsset.url, archivePath, fetchImpl, matchingAsset.size, signal);
23585
+ } catch (err) {
23586
+ error48(`[lsp] download ${spec.id} failed: ${err}`);
23587
+ return null;
23588
+ }
23589
+ let archiveSha256;
23590
+ try {
23591
+ archiveSha256 = await sha256OfFile(archivePath);
23592
+ } catch (err) {
23593
+ error48(`[lsp] hash ${spec.id} failed: ${err}`);
23594
+ try {
23595
+ unlinkSync3(archivePath);
23596
+ } catch {}
23597
+ return null;
23598
+ }
23599
+ log(`[lsp] ${spec.id} ${tag} sha256=${archiveSha256}`);
23600
+ const previousMeta = readInstalledMetaIn(ghPackageDir(spec));
23601
+ if (previousMeta && previousMeta.version === tag && previousMeta.sha256) {
23602
+ if (previousMeta.sha256 !== archiveSha256) {
23603
+ error48(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch \u2014 refusing install. ` + `Previously installed sha256=${previousMeta.sha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding. ` + `Run \`aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
23604
+ try {
23605
+ unlinkSync3(archivePath);
23606
+ } catch {}
23607
+ return null;
23608
+ }
23609
+ }
23610
+ try {
23611
+ extractArchiveSafely(archivePath, extractDir, expected.archive);
23612
+ } catch (err) {
23613
+ error48(`[lsp] extract ${spec.id} failed: ${err}`);
23614
+ return null;
23615
+ } finally {
23616
+ try {
23617
+ unlinkSync3(archivePath);
23618
+ } catch {}
23619
+ }
23620
+ const innerBinaryPath = join9(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
23621
+ if (!existsSync6(innerBinaryPath)) {
23622
+ error48(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
23623
+ return null;
23624
+ }
23625
+ const targetBinary = ghBinaryPath(spec, platform2);
23626
+ mkdirSync3(dirname3(targetBinary), { recursive: true });
23627
+ try {
23628
+ copyFileSync(innerBinaryPath, targetBinary);
23629
+ if (platform2 !== "win32") {
23630
+ const { chmodSync: chmodSync2 } = await import("fs");
23631
+ chmodSync2(targetBinary, 493);
23632
+ }
23633
+ } catch (err) {
23634
+ error48(`[lsp] ${spec.id}: failed to place binary at ${targetBinary}: ${err}`);
23635
+ return null;
23636
+ }
23637
+ log(`[lsp] installed ${spec.id} ${tag} at ${targetBinary}`);
23638
+ return archiveSha256;
23639
+ }
23640
+ async function ensureGithubInstalled(spec, config2, fetchImpl, platform2, arch, signal) {
23641
+ const outcome = await withInstallLock(spec.githubRepo, async () => {
23642
+ const { tag, assets, blockedByGrace, reason } = await resolveTargetTag(spec, config2, fetchImpl, signal);
23643
+ if (!tag) {
23644
+ const installed = isGithubInstalled(spec, platform2);
23645
+ if (installed) {
23646
+ warn(`[lsp] no eligible release of ${spec.githubRepo} (grace=${config2.graceDays}d); keeping existing install`);
23647
+ return { started: false, reason: "kept existing install" };
23648
+ }
23649
+ const fallbackReason = reason ?? (blockedByGrace ? `all releases are within ${config2.graceDays}-day grace window` : "github releases probe failed");
23650
+ warn(`[lsp] skipping ${spec.id}: ${fallbackReason}`);
23651
+ return { started: false, reason: fallbackReason };
23652
+ }
23653
+ if (isGithubInstalled(spec, platform2)) {
23654
+ const installedMeta = readInstalledMetaIn(ghPackageDir(spec));
23655
+ if (installedMeta && installedMeta.version === tag) {
23656
+ return { started: false, reason: "already installed" };
23657
+ }
23658
+ if (installedMeta) {
23659
+ log(`[lsp] reinstalling ${spec.id}: cached ${installedMeta.version} \u2260 target ${tag}`);
23660
+ } else {
23661
+ log(`[lsp] reinstalling ${spec.id}@${tag}: no installed-version metadata recorded`);
23662
+ }
23663
+ }
23664
+ const archiveSha256 = await downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal).catch((err) => {
23665
+ error48(`[lsp] github install ${spec.id} crashed: ${err}`);
23666
+ return null;
23667
+ });
23668
+ if (!archiveSha256) {
23669
+ return { started: true, reason: "install failed (see plugin log)" };
23670
+ }
23671
+ writeInstalledMetaIn(ghPackageDir(spec), tag, archiveSha256);
23672
+ return { started: true };
23673
+ });
23674
+ if (outcome === null) {
23675
+ return { started: false, reason: "another install in progress" };
23676
+ }
23677
+ return outcome;
23678
+ }
23679
+ var inFlightGithubInstalls = new Set;
23680
+ function trackInFlightGithubInstall(controller, promise2) {
23681
+ const entry = { controller, promise: promise2 };
23682
+ inFlightGithubInstalls.add(entry);
23683
+ promise2.then(() => inFlightGithubInstalls.delete(entry), () => inFlightGithubInstalls.delete(entry));
23684
+ return promise2;
23685
+ }
23686
+ async function abortInFlightGithubInstalls() {
23687
+ const installs = Array.from(inFlightGithubInstalls);
23688
+ for (const install of installs) {
23689
+ install.controller.abort();
23690
+ }
23691
+ await Promise.allSettled(installs.map((install) => install.promise));
23692
+ }
23693
+ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
23694
+ const cachedBinDirs = [];
23695
+ const skipped = [];
23696
+ const installPromises = [];
23697
+ let installsStarted = 0;
23698
+ const host = detectHostPlatform();
23699
+ if (!host) {
23700
+ for (const spec of GITHUB_LSP_TABLE) {
23701
+ try {
23702
+ if (existsSync6(ghBinDir(spec))) {
23703
+ cachedBinDirs.push(ghBinDir(spec));
23704
+ }
23705
+ } catch {}
23706
+ }
23707
+ return {
23708
+ cachedBinDirs,
23709
+ installsStarted: 0,
23710
+ skipped,
23711
+ installsComplete: Promise.resolve()
23712
+ };
23713
+ }
23714
+ for (const spec of GITHUB_LSP_TABLE) {
23715
+ if (isGithubInstalled(spec, host.platform)) {
23716
+ cachedBinDirs.push(ghBinDir(spec));
23717
+ }
23718
+ if (config2.disabled.has(spec.id)) {
23719
+ skipped.push({ id: spec.id, reason: "disabled by config" });
23720
+ continue;
23721
+ }
23722
+ if (!config2.autoInstall) {
23723
+ skipped.push({ id: spec.id, reason: "auto_install: false" });
23724
+ continue;
23725
+ }
23726
+ if (!relevantServers.has(spec.id)) {
23727
+ skipped.push({ id: spec.id, reason: "not relevant to project" });
23728
+ continue;
23729
+ }
23730
+ installsStarted += 1;
23731
+ const controller = new AbortController;
23732
+ const promise2 = ensureGithubInstalled(spec, config2, fetchImpl, host.platform, host.arch, controller.signal).then((outcome) => {
23733
+ if (!outcome.started)
23734
+ installsStarted -= 1;
23735
+ if (outcome.reason && outcome.reason !== "already installed") {
23736
+ skipped.push({ id: spec.id, reason: outcome.reason });
23737
+ }
23738
+ }, (err) => {
23739
+ installsStarted -= 1;
23740
+ const reason = err instanceof Error ? err.message : String(err);
23741
+ skipped.push({ id: spec.id, reason: `install error: ${reason}` });
23742
+ error48(`[lsp] github install ${spec.id} promise rejected: ${reason}`);
23743
+ });
23744
+ installPromises.push(trackInFlightGithubInstall(controller, promise2));
23745
+ }
23746
+ return {
23747
+ cachedBinDirs,
23748
+ get installsStarted() {
23749
+ return installsStarted;
23750
+ },
23751
+ skipped,
23752
+ installsComplete: Promise.all(installPromises).then(() => {})
23753
+ };
23754
+ }
23755
+ function discoverRelevantGithubServers(projectRoot) {
23756
+ const extToServerIds = {
23757
+ c: ["clangd"],
23758
+ "c++": ["clangd"],
23759
+ cc: ["clangd"],
23760
+ cpp: ["clangd"],
23761
+ cxx: ["clangd"],
23762
+ h: ["clangd"],
23763
+ "h++": ["clangd"],
23764
+ hpp: ["clangd"],
23765
+ hh: ["clangd"],
23766
+ hxx: ["clangd"],
23767
+ lua: ["lua-ls"],
23768
+ zig: ["zls"],
23769
+ zon: ["zls"],
23770
+ typ: ["tinymist"],
23771
+ typc: ["tinymist"],
23772
+ tex: ["texlab"],
23773
+ bib: ["texlab"]
23774
+ };
23775
+ const rootMarkers = {
23776
+ clangd: ["compile_commands.json", "compile_flags.txt", ".clangd"],
23777
+ "lua-ls": [".luarc.json", ".luarc.jsonc", ".stylua.toml", "stylua.toml"],
23778
+ zls: ["build.zig"],
23779
+ tinymist: ["typst.toml"],
23780
+ texlab: [".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]
23781
+ };
23782
+ const relevant = new Set;
23783
+ for (const spec of GITHUB_LSP_TABLE) {
23784
+ if (hasRootMarker(projectRoot, rootMarkers[spec.id]))
23785
+ relevant.add(spec.id);
23786
+ }
23787
+ const extensions = relevantExtensionsInProject(projectRoot, extToServerIds);
23788
+ for (const ext of extensions) {
23789
+ for (const id of extToServerIds[ext] ?? []) {
23790
+ relevant.add(id);
23791
+ }
23792
+ }
23793
+ return relevant;
23794
+ }
23795
+
23796
+ // src/metadata-store.ts
23797
+ var pendingStore = new Map;
23798
+ var STALE_TIMEOUT_MS = 15 * 60 * 1000;
23799
+ function makeKey(sessionID, callID) {
23800
+ return `${sessionID}:${callID}`;
23801
+ }
23802
+ function cleanupStaleEntries() {
23803
+ const now = Date.now();
23804
+ for (const [key, entry] of pendingStore) {
23805
+ if (now - entry.storedAt > STALE_TIMEOUT_MS) {
23806
+ pendingStore.delete(key);
23807
+ }
23808
+ }
23809
+ }
23810
+ function storeToolMetadata(sessionID, callID, data) {
23811
+ cleanupStaleEntries();
23812
+ pendingStore.set(makeKey(sessionID, callID), {
23813
+ ...data,
23814
+ storedAt: Date.now()
23815
+ });
23816
+ }
23817
+ function consumeToolMetadata(sessionID, callID) {
23818
+ const key = makeKey(sessionID, callID);
23819
+ const stored = pendingStore.get(key);
23820
+ if (stored) {
23821
+ pendingStore.delete(key);
23822
+ const { storedAt: _, ...data } = stored;
23823
+ return data;
23824
+ }
23825
+ return;
23826
+ }
23827
+
23828
+ // src/normalize-schemas.ts
23829
+ import { tool } from "@opencode-ai/plugin";
23830
+ function stripRootJsonSchemaFields(jsonSchema) {
23831
+ const { $schema: _schema, ...rest } = jsonSchema;
23832
+ return rest;
23833
+ }
23834
+ function attachJsonSchemaOverride(schema) {
23835
+ if (schema._zod.toJSONSchema) {
23836
+ return;
23837
+ }
23838
+ schema._zod.toJSONSchema = () => {
23839
+ const originalOverride = schema._zod.toJSONSchema;
23840
+ delete schema._zod.toJSONSchema;
23841
+ try {
23842
+ return stripRootJsonSchemaFields(tool.schema.toJSONSchema(schema));
23843
+ } finally {
23844
+ schema._zod.toJSONSchema = originalOverride;
23845
+ }
23846
+ };
23847
+ }
23848
+ function normalizeToolArgSchemas(toolDefinition) {
23849
+ for (const schema of Object.values(toolDefinition.args)) {
23850
+ attachJsonSchemaOverride(schema);
23851
+ }
23852
+ return toolDefinition;
23853
+ }
23854
+ function normalizeToolMap(tools) {
23855
+ for (const def of Object.values(tools)) {
23856
+ normalizeToolArgSchemas(def);
23857
+ }
23858
+ return tools;
23859
+ }
23860
+
23861
+ // src/notifications.ts
23862
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
23863
+ import { homedir as homedir6, platform as platform2 } from "os";
23864
+ import { join as join10 } from "path";
23865
+ function isTuiMode() {
23866
+ return process.env.OPENCODE_CLIENT === "cli";
23867
+ }
23868
+ async function showTuiToast(client, title, message, variant = "info", duration3 = 8000) {
23869
+ const c = client;
23870
+ if (typeof c.tui?.showToast !== "function")
23871
+ return false;
23872
+ try {
23873
+ await c.tui.showToast({ body: { title, message, variant, duration: duration3 } });
23874
+ return true;
23875
+ } catch {
23876
+ return false;
23877
+ }
23878
+ }
23879
+ var AFT_MARKER = "\uD83D\uDD27 AFT:";
23880
+ var FEATURE_MARKER = `${AFT_MARKER} New in`;
23881
+ var WARNING_MARKER = `${AFT_MARKER} \u26A0\uFE0F`;
23882
+ var STATUS_MARKER = `${AFT_MARKER} \u2705`;
23883
+ var WARNED_TOOLS_FILE = "warned_tools.json";
23884
+ function getDesktopStatePath() {
23885
+ const os2 = platform2();
23886
+ const home = homedir6();
23887
+ if (os2 === "darwin") {
23888
+ return join10(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
23889
+ }
23890
+ if (os2 === "linux") {
23891
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join10(home, ".config");
23892
+ return join10(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
23893
+ }
23894
+ if (os2 === "win32") {
23895
+ const appData = process.env.APPDATA || join10(home, "AppData", "Roaming");
23896
+ return join10(appData, "ai.opencode.desktop", "opencode.global.dat");
23897
+ }
23898
+ return null;
23899
+ }
23900
+ function readDesktopState(directory) {
23901
+ const statePath = getDesktopStatePath();
23902
+ if (!statePath || !existsSync7(statePath)) {
23903
+ return { sessionId: null, serverUrl: null };
23904
+ }
23905
+ try {
23906
+ const raw = readFileSync5(statePath, "utf-8");
23907
+ const state = JSON.parse(raw);
23908
+ let serverUrl = null;
23909
+ const serverStr = state.server;
23910
+ if (typeof serverStr === "string") {
23911
+ try {
23912
+ const serverState = JSON.parse(serverStr);
23913
+ if (typeof serverState.currentSidecarUrl === "string") {
23914
+ serverUrl = serverState.currentSidecarUrl;
23915
+ }
23916
+ } catch {}
23917
+ }
23918
+ let sessionId = null;
23919
+ const layoutPage = state["layout.page"];
23920
+ if (typeof layoutPage === "string") {
23921
+ const parsed = JSON.parse(layoutPage);
23922
+ const lastProjectSession = parsed.lastProjectSession;
23923
+ if (lastProjectSession) {
23924
+ const entry = lastProjectSession[directory];
23925
+ sessionId = entry?.id ?? null;
23926
+ }
23927
+ }
23928
+ return { sessionId, serverUrl };
23929
+ } catch {
23930
+ return { sessionId: null, serverUrl: null };
23931
+ }
23932
+ }
23933
+ function getServerAuth() {
23934
+ const password = process.env.OPENCODE_SERVER_PASSWORD;
23935
+ if (!password)
23936
+ return;
23937
+ const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
23938
+ return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
23939
+ }
23940
+ async function getSessionMessages(client, sessionId) {
23941
+ try {
23942
+ const c = client;
23943
+ if (typeof c.session?.messages === "function") {
23944
+ const result = await c.session.messages({ path: { id: sessionId } });
23945
+ return result?.data ?? [];
23946
+ }
23947
+ } catch {}
23948
+ return [];
23949
+ }
23950
+ async function sendIgnoredMessage(client, sessionId, text) {
23951
+ try {
23952
+ const c = client;
23953
+ const promptInput = {
23954
+ path: { id: sessionId },
23955
+ body: {
23956
+ noReply: true,
23957
+ parts: [{ type: "text", text, ignored: true }]
23958
+ }
23959
+ };
23960
+ if (typeof c.session?.prompt === "function") {
23961
+ await Promise.resolve(c.session.prompt(promptInput));
23962
+ return true;
23963
+ }
23964
+ if (typeof c.session?.promptAsync === "function") {
23965
+ await c.session.promptAsync(promptInput);
23966
+ return true;
23967
+ }
23968
+ } catch (err) {
23969
+ log(`[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
23970
+ }
23971
+ return false;
23972
+ }
23973
+ async function deleteMessage(serverUrl, sessionId, messageId) {
23974
+ const auth = getServerAuth();
23975
+ const url2 = `${serverUrl}/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`;
23976
+ try {
23977
+ const response = await fetch(url2, {
23978
+ method: "DELETE",
23979
+ headers: auth ? { Authorization: auth } : {},
23980
+ signal: AbortSignal.timeout(1e4)
23981
+ });
23982
+ return response.ok;
23983
+ } catch {
21996
23984
  return false;
21997
23985
  }
21998
23986
  }
@@ -22009,10 +23997,10 @@ async function sendWarning(opts, message) {
22009
23997
  }
22010
23998
  async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
22011
23999
  if (storageDir) {
22012
- const versionFile = join4(storageDir, "last_announced_version");
24000
+ const versionFile = join10(storageDir, "last_announced_version");
22013
24001
  try {
22014
- if (existsSync3(versionFile)) {
22015
- const lastVersion = readFileSync2(versionFile, "utf-8").trim();
24002
+ if (existsSync7(versionFile)) {
24003
+ const lastVersion = readFileSync5(versionFile, "utf-8").trim();
22016
24004
  if (lastVersion === version2)
22017
24005
  return;
22018
24006
  }
@@ -22032,17 +24020,17 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
22032
24020
  }
22033
24021
  if (storageDir) {
22034
24022
  try {
22035
- mkdirSync2(storageDir, { recursive: true });
22036
- writeFileSync(join4(storageDir, "last_announced_version"), version2);
24023
+ mkdirSync4(storageDir, { recursive: true });
24024
+ writeFileSync4(join10(storageDir, "last_announced_version"), version2);
22037
24025
  } catch {}
22038
24026
  }
22039
24027
  }
22040
24028
  function readWarnedTools(storageDir) {
22041
24029
  try {
22042
- const warnedToolsPath = join4(storageDir, WARNED_TOOLS_FILE);
22043
- if (!existsSync3(warnedToolsPath))
24030
+ const warnedToolsPath = join10(storageDir, WARNED_TOOLS_FILE);
24031
+ if (!existsSync7(warnedToolsPath))
22044
24032
  return {};
22045
- const parsed = JSON.parse(readFileSync2(warnedToolsPath, "utf-8"));
24033
+ const parsed = JSON.parse(readFileSync5(warnedToolsPath, "utf-8"));
22046
24034
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
22047
24035
  return {};
22048
24036
  const warned = {};
@@ -22058,9 +24046,9 @@ function readWarnedTools(storageDir) {
22058
24046
  }
22059
24047
  function writeWarnedTools(storageDir, warned) {
22060
24048
  try {
22061
- mkdirSync2(storageDir, { recursive: true });
22062
- const warnedToolsPath = join4(storageDir, WARNED_TOOLS_FILE);
22063
- writeFileSync(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
24049
+ mkdirSync4(storageDir, { recursive: true });
24050
+ const warnedToolsPath = join10(storageDir, WARNED_TOOLS_FILE);
24051
+ writeFileSync4(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
22064
24052
  `);
22065
24053
  } catch {}
22066
24054
  }
@@ -22151,10 +24139,37 @@ async function cleanupWarnings(opts) {
22151
24139
  }
22152
24140
 
22153
24141
  // src/onnx-runtime.ts
22154
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, unlinkSync as unlinkSync2 } from "fs";
22155
- import { join as join5 } from "path";
24142
+ import { execFileSync as execFileSync2 } from "child_process";
24143
+ import { createHash as createHash3 } from "crypto";
24144
+ import {
24145
+ chmodSync as chmodSync2,
24146
+ closeSync as closeSync2,
24147
+ copyFileSync as copyFileSync2,
24148
+ createWriteStream as createWriteStream2,
24149
+ existsSync as existsSync8,
24150
+ lstatSync as lstatSync2,
24151
+ mkdirSync as mkdirSync5,
24152
+ openSync as openSync2,
24153
+ readdirSync as readdirSync3,
24154
+ readFileSync as readFileSync6,
24155
+ readlinkSync as readlinkSync2,
24156
+ realpathSync as realpathSync2,
24157
+ rmSync as rmSync3,
24158
+ statSync as statSync5,
24159
+ symlinkSync,
24160
+ unlinkSync as unlinkSync4,
24161
+ writeFileSync as writeFileSync5
24162
+ } from "fs";
24163
+ import { dirname as dirname4, join as join11, relative as relative2, resolve as resolve3 } from "path";
24164
+ import { Readable as Readable2 } from "stream";
24165
+ import { pipeline as pipeline2 } from "stream/promises";
22156
24166
  var ORT_VERSION = "1.24.4";
22157
24167
  var ORT_REPO = "microsoft/onnxruntime";
24168
+ var MAX_DOWNLOAD_BYTES2 = 256 * 1024 * 1024;
24169
+ var MAX_EXTRACT_BYTES2 = 1 * 1024 * 1024 * 1024;
24170
+ var ONNX_LOCK_FILE = ".aft-onnx-installing";
24171
+ var ONNX_INSTALLED_META_FILE = ".aft-onnx-installed";
24172
+ var STALE_LOCK_MS2 = 30 * 60 * 1000;
22158
24173
  var ORT_PLATFORM_MAP = {
22159
24174
  darwin: {
22160
24175
  arm64: {
@@ -22208,11 +24223,27 @@ function getManualInstallHint() {
22208
24223
  }
22209
24224
  async function ensureOnnxRuntime(storageDir) {
22210
24225
  const info = getPlatformInfo();
22211
- const ortDir = join5(storageDir, "onnxruntime", ORT_VERSION);
22212
- const libPath = join5(ortDir, info?.libName ?? "libonnxruntime.dylib");
22213
- if (existsSync4(libPath)) {
22214
- log(`ONNX Runtime found at ${ortDir}`);
22215
- return ortDir;
24226
+ const ortDir = join11(storageDir, "onnxruntime", ORT_VERSION);
24227
+ const libPath = join11(ortDir, info?.libName ?? "libonnxruntime.dylib");
24228
+ if (existsSync8(libPath)) {
24229
+ const meta3 = readOnnxInstalledMeta(ortDir);
24230
+ if (meta3?.sha256) {
24231
+ try {
24232
+ const currentHash = sha256File(libPath);
24233
+ if (currentHash !== meta3.sha256) {
24234
+ error48(`ONNX Runtime at ${ortDir}: TOFU sha256 mismatch \u2014 refusing to use ` + `tampered binary. Recorded ${meta3.sha256}, current ${currentHash}. ` + `Run \`aft doctor --clear\` to re-download from scratch.`);
24235
+ } else {
24236
+ log(`ONNX Runtime found at ${ortDir} (TOFU verified)`);
24237
+ return ortDir;
24238
+ }
24239
+ } catch (err) {
24240
+ warn(`Could not verify ONNX Runtime hash at ${ortDir}: ${err}`);
24241
+ return ortDir;
24242
+ }
24243
+ } else {
24244
+ log(`ONNX Runtime found at ${ortDir} (no recorded hash, accepting)`);
24245
+ return ortDir;
24246
+ }
22216
24247
  }
22217
24248
  const systemPath = findSystemOnnxRuntime(info?.libName);
22218
24249
  if (systemPath) {
@@ -22223,7 +24254,18 @@ async function ensureOnnxRuntime(storageDir) {
22223
24254
  warn(`ONNX Runtime auto-download not available for ${process.platform}/${process.arch}. Install manually: ${getManualInstallHint()}`);
22224
24255
  return null;
22225
24256
  }
22226
- return downloadOnnxRuntime(info, ortDir);
24257
+ const onnxBaseDir = join11(storageDir, "onnxruntime");
24258
+ mkdirSync5(onnxBaseDir, { recursive: true });
24259
+ const lockPath2 = join11(onnxBaseDir, ONNX_LOCK_FILE);
24260
+ if (!acquireLock(lockPath2)) {
24261
+ warn(`ONNX Runtime install already in progress in another process (lock: ${lockPath2}). Skipping.`);
24262
+ return null;
24263
+ }
24264
+ try {
24265
+ return await downloadOnnxRuntime(info, ortDir);
24266
+ } finally {
24267
+ releaseLock(lockPath2);
24268
+ }
22227
24269
  }
22228
24270
  function findSystemOnnxRuntime(libName) {
22229
24271
  if (!libName)
@@ -22235,45 +24277,124 @@ function findSystemOnnxRuntime(libName) {
22235
24277
  searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
22236
24278
  }
22237
24279
  for (const dir of searchPaths) {
22238
- if (existsSync4(join5(dir, libName))) {
24280
+ if (existsSync8(join11(dir, libName))) {
22239
24281
  return dir;
22240
24282
  }
22241
24283
  }
22242
24284
  return null;
22243
24285
  }
24286
+ async function downloadFileWithCap(url2, destPath) {
24287
+ const controller = new AbortController;
24288
+ const timeout = setTimeout(() => controller.abort(), 300000);
24289
+ try {
24290
+ const res = await fetch(url2, {
24291
+ headers: { accept: "application/octet-stream" },
24292
+ redirect: "follow",
24293
+ signal: controller.signal
24294
+ });
24295
+ if (!res.ok || !res.body) {
24296
+ throw new Error(`download failed (HTTP ${res.status})`);
24297
+ }
24298
+ const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
24299
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
24300
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
24301
+ }
24302
+ mkdirSync5(dirname4(destPath), { recursive: true });
24303
+ let bytesWritten = 0;
24304
+ const guard = new TransformStream({
24305
+ transform(chunk, transformController) {
24306
+ bytesWritten += chunk.byteLength;
24307
+ if (bytesWritten > MAX_DOWNLOAD_BYTES2) {
24308
+ transformController.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES2} bytes after streaming (server lied about size or sent unbounded body)`));
24309
+ return;
24310
+ }
24311
+ transformController.enqueue(chunk);
24312
+ }
24313
+ });
24314
+ const guarded = res.body.pipeThrough(guard);
24315
+ const nodeStream = Readable2.fromWeb(guarded);
24316
+ await pipeline2(nodeStream, createWriteStream2(destPath), { signal: controller.signal });
24317
+ } catch (err) {
24318
+ try {
24319
+ unlinkSync4(destPath);
24320
+ } catch {}
24321
+ throw err;
24322
+ } finally {
24323
+ clearTimeout(timeout);
24324
+ }
24325
+ }
24326
+ function validateExtractedTree(stagingRoot) {
24327
+ const realRoot = realpathSync2(stagingRoot);
24328
+ let totalBytes = 0;
24329
+ const walk = (dir) => {
24330
+ const entries = readdirSync3(dir);
24331
+ for (const entry of entries) {
24332
+ const fullPath = join11(dir, entry);
24333
+ const lst = lstatSync2(fullPath);
24334
+ if (lst.isSymbolicLink()) {
24335
+ const linkTarget = readlinkSync2(fullPath);
24336
+ const resolvedTarget = resolve3(dirname4(fullPath), linkTarget);
24337
+ const rel2 = relative2(realRoot, resolvedTarget);
24338
+ if (rel2.startsWith("..") || process.platform !== "win32" && rel2.startsWith("/")) {
24339
+ throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
24340
+ }
24341
+ continue;
24342
+ }
24343
+ const rel = relative2(realRoot, fullPath);
24344
+ if (rel.startsWith("..") || process.platform !== "win32" && rel.startsWith("/")) {
24345
+ throw new Error(`extracted entry ${fullPath} escapes staging root`);
24346
+ }
24347
+ if (lst.isDirectory()) {
24348
+ walk(fullPath);
24349
+ continue;
24350
+ }
24351
+ if (lst.isFile()) {
24352
+ totalBytes += lst.size;
24353
+ if (totalBytes > MAX_EXTRACT_BYTES2) {
24354
+ throw new Error(`extracted size ${totalBytes} exceeds max ${MAX_EXTRACT_BYTES2} (decompression bomb defense)`);
24355
+ }
24356
+ }
24357
+ }
24358
+ };
24359
+ walk(realRoot);
24360
+ }
22244
24361
  async function downloadOnnxRuntime(info, targetDir) {
22245
24362
  const url2 = `https://github.com/${ORT_REPO}/releases/download/v${ORT_VERSION}/${info.assetName}.${info.archiveType === "tgz" ? "tgz" : "zip"}`;
22246
24363
  log(`Downloading ONNX Runtime v${ORT_VERSION} for ${process.platform}/${process.arch}...`);
24364
+ const tmpDir = `${targetDir}.tmp.${process.pid}.${Date.now().toString(36)}`;
22247
24365
  try {
22248
- const tmpDir = `${targetDir}.tmp.${process.pid}`;
22249
- mkdirSync3(tmpDir, { recursive: true });
22250
- const archivePath = join5(tmpDir, `onnxruntime.${info.archiveType}`);
22251
- const { execFileSync } = await import("child_process");
22252
- execFileSync("curl", ["-fsSL", url2, "-o", archivePath], {
22253
- stdio: "pipe",
22254
- timeout: 120000
22255
- });
24366
+ mkdirSync5(tmpDir, { recursive: true });
24367
+ const archivePath = join11(tmpDir, `onnxruntime.${info.archiveType}`);
24368
+ await downloadFileWithCap(url2, archivePath);
24369
+ const archiveSha256 = sha256File(archivePath);
24370
+ log(`ONNX Runtime archive sha256=${archiveSha256}`);
22256
24371
  if (info.archiveType === "tgz") {
22257
- execFileSync("tar", ["xzf", archivePath, "-C", tmpDir], { stdio: "pipe" });
24372
+ execFileSync2("tar", ["xzf", archivePath, "-C", tmpDir], {
24373
+ stdio: "pipe",
24374
+ timeout: 120000
24375
+ });
22258
24376
  } else {
22259
24377
  await extractZipArchive(archivePath, tmpDir);
22260
24378
  }
22261
- const extractedDir = join5(tmpDir, info.assetName, "lib");
22262
- if (!existsSync4(extractedDir)) {
24379
+ try {
24380
+ unlinkSync4(archivePath);
24381
+ } catch {}
24382
+ validateExtractedTree(tmpDir);
24383
+ const extractedDir = join11(tmpDir, info.assetName, "lib");
24384
+ if (!existsSync8(extractedDir)) {
22263
24385
  throw new Error(`Expected directory not found: ${extractedDir}`);
22264
24386
  }
22265
- mkdirSync3(targetDir, { recursive: true });
22266
- const libFiles = readdirSync(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
22267
- const { lstatSync, symlinkSync, readlinkSync, copyFileSync: cpFile } = await import("fs");
24387
+ mkdirSync5(targetDir, { recursive: true });
24388
+ const libFiles = readdirSync3(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
22268
24389
  const realFiles = [];
22269
24390
  const symlinks = [];
22270
24391
  for (const libFile of libFiles) {
22271
- const src = join5(extractedDir, libFile);
24392
+ const src = join11(extractedDir, libFile);
22272
24393
  try {
22273
- const stat = lstatSync(src);
24394
+ const stat = lstatSync2(src);
22274
24395
  log(`ORT extract: ${libFile} \u2014 isSymlink=${stat.isSymbolicLink()}, isFile=${stat.isFile()}, size=${stat.size}`);
22275
24396
  if (stat.isSymbolicLink()) {
22276
- symlinks.push({ name: libFile, target: readlinkSync(src) });
24397
+ symlinks.push({ name: libFile, target: readlinkSync2(src) });
22277
24398
  } else {
22278
24399
  realFiles.push(libFile);
22279
24400
  }
@@ -22283,10 +24404,10 @@ async function downloadOnnxRuntime(info, targetDir) {
22283
24404
  }
22284
24405
  }
22285
24406
  for (const libFile of realFiles) {
22286
- const src = join5(extractedDir, libFile);
22287
- const dst = join5(targetDir, libFile);
24407
+ const src = join11(extractedDir, libFile);
24408
+ const dst = join11(targetDir, libFile);
22288
24409
  try {
22289
- cpFile(src, dst);
24410
+ copyFileSync2(src, dst);
22290
24411
  if (process.platform !== "win32") {
22291
24412
  chmodSync2(dst, 493);
22292
24413
  }
@@ -22295,62 +24416,179 @@ async function downloadOnnxRuntime(info, targetDir) {
22295
24416
  }
22296
24417
  }
22297
24418
  for (const link of symlinks) {
22298
- const dst = join5(targetDir, link.name);
24419
+ const dst = join11(targetDir, link.name);
22299
24420
  try {
22300
- unlinkSync2(dst);
24421
+ unlinkSync4(dst);
22301
24422
  } catch {}
22302
24423
  symlinkSync(link.target, dst);
22303
24424
  }
22304
- const { rmSync } = await import("fs");
22305
- rmSync(tmpDir, { recursive: true, force: true });
24425
+ const libPath = join11(targetDir, info.libName);
24426
+ let libHash = null;
24427
+ try {
24428
+ libHash = sha256File(libPath);
24429
+ } catch (err) {
24430
+ warn(`Could not hash newly-installed ONNX library at ${libPath}: ${err}`);
24431
+ }
24432
+ writeOnnxInstalledMeta(targetDir, ORT_VERSION, libHash, archiveSha256);
24433
+ rmSync3(tmpDir, { recursive: true, force: true });
22306
24434
  log(`ONNX Runtime v${ORT_VERSION} installed to ${targetDir}`);
22307
24435
  return targetDir;
22308
24436
  } catch (err) {
22309
24437
  error48(`Failed to download ONNX Runtime: ${err}`);
22310
24438
  try {
22311
- const { rmSync } = await import("fs");
22312
- rmSync(`${targetDir}.tmp.${process.pid}`, { recursive: true, force: true });
24439
+ rmSync3(tmpDir, { recursive: true, force: true });
24440
+ } catch {}
24441
+ try {
24442
+ rmSync3(targetDir, { recursive: true, force: true });
22313
24443
  } catch {}
22314
24444
  return null;
22315
24445
  }
22316
24446
  }
22317
24447
  async function extractZipArchive(archivePath, destinationDir) {
22318
- const { execFileSync } = await import("child_process");
22319
24448
  if (process.platform === "win32") {
22320
- let powershellError;
24449
+ execFileSync2("tar.exe", ["-xf", archivePath, "-C", destinationDir], {
24450
+ stdio: "pipe",
24451
+ timeout: 120000
24452
+ });
24453
+ return;
24454
+ }
24455
+ execFileSync2("unzip", ["-q", archivePath, "-d", destinationDir], {
24456
+ stdio: "pipe",
24457
+ timeout: 120000
24458
+ });
24459
+ }
24460
+ function writeOnnxInstalledMeta(installDir, version2, sha256, archiveSha256) {
24461
+ try {
24462
+ const meta3 = {
24463
+ version: version2,
24464
+ installedAt: new Date().toISOString(),
24465
+ ...sha256 ? { sha256 } : {},
24466
+ archiveSha256
24467
+ };
24468
+ writeFileSync5(join11(installDir, ONNX_INSTALLED_META_FILE), JSON.stringify(meta3), "utf8");
24469
+ } catch (err) {
24470
+ log(`[onnx] failed to write installed-meta in ${installDir}: ${err}`);
24471
+ }
24472
+ }
24473
+ function readOnnxInstalledMeta(installDir) {
24474
+ const path2 = join11(installDir, ONNX_INSTALLED_META_FILE);
24475
+ try {
24476
+ if (!statSync5(path2).isFile())
24477
+ return null;
24478
+ const raw = readFileSync6(path2, "utf8");
24479
+ const parsed = JSON.parse(raw);
24480
+ if (typeof parsed.version !== "string" || parsed.version.length === 0)
24481
+ return null;
24482
+ return {
24483
+ version: parsed.version,
24484
+ installedAt: typeof parsed.installedAt === "string" ? parsed.installedAt : "",
24485
+ ...typeof parsed.sha256 === "string" && parsed.sha256.length > 0 ? { sha256: parsed.sha256 } : {},
24486
+ ...typeof parsed.archiveSha256 === "string" && parsed.archiveSha256.length > 0 ? { archiveSha256: parsed.archiveSha256 } : {}
24487
+ };
24488
+ } catch {
24489
+ return null;
24490
+ }
24491
+ }
24492
+ function sha256File(path2) {
24493
+ const hash2 = createHash3("sha256");
24494
+ hash2.update(readFileSync6(path2));
24495
+ return hash2.digest("hex");
24496
+ }
24497
+ function acquireLock(lockPath2) {
24498
+ const tryClaim = () => {
22321
24499
  try {
22322
- execFileSync("powershell.exe", [
22323
- "-NoProfile",
22324
- "-NonInteractive",
22325
- "-ExecutionPolicy",
22326
- "Bypass",
22327
- "-Command",
22328
- "& { Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force }",
22329
- archivePath,
22330
- destinationDir
22331
- ], { stdio: "pipe", timeout: 120000 });
22332
- return;
24500
+ const fd = openSync2(lockPath2, "wx");
24501
+ try {
24502
+ writeFileSync5(fd, `${process.pid}
24503
+ ${new Date().toISOString()}
24504
+ `);
24505
+ } finally {
24506
+ closeSync2(fd);
24507
+ }
24508
+ return true;
22333
24509
  } catch (err) {
22334
- powershellError = err;
22335
- warn(`PowerShell Expand-Archive failed, falling back to cmd/tar: ${String(err)}`);
24510
+ const code = err.code;
24511
+ if (code === "EEXIST")
24512
+ return false;
24513
+ warn(`[onnx] unexpected error acquiring lock ${lockPath2}: ${err}`);
24514
+ return false;
22336
24515
  }
24516
+ };
24517
+ if (tryClaim())
24518
+ return true;
24519
+ let owningPid = null;
24520
+ let lockMtimeMs = 0;
24521
+ try {
24522
+ const raw = readFileSync6(lockPath2, "utf8");
24523
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
24524
+ const parsed = Number.parseInt(firstLine, 10);
24525
+ if (Number.isFinite(parsed) && parsed > 0)
24526
+ owningPid = parsed;
24527
+ lockMtimeMs = statSync5(lockPath2).mtimeMs;
24528
+ } catch {
24529
+ return tryClaim();
24530
+ }
24531
+ const age = Date.now() - lockMtimeMs;
24532
+ const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS2;
24533
+ const skipLiveness = process.platform === "win32";
24534
+ const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive2(owningPid);
24535
+ if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
24536
+ return false;
24537
+ }
24538
+ log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
24539
+ try {
24540
+ unlinkSync4(lockPath2);
24541
+ } catch {}
24542
+ return tryClaim();
24543
+ }
24544
+ function releaseLock(lockPath2) {
24545
+ try {
24546
+ let owningPid = null;
22337
24547
  try {
22338
- execFileSync("cmd.exe", ["/d", "/s", "/c", `tar -xf "${archivePath}" -C "${destinationDir}"`], { stdio: "pipe", timeout: 120000 });
24548
+ const raw = readFileSync6(lockPath2, "utf8");
24549
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
24550
+ const parsed = Number.parseInt(firstLine, 10);
24551
+ if (Number.isFinite(parsed) && parsed > 0)
24552
+ owningPid = parsed;
24553
+ } catch (readErr) {
24554
+ const code = readErr.code;
24555
+ if (code === "ENOENT")
24556
+ return;
24557
+ warn(`[onnx] could not read lock ${lockPath2} during release: ${readErr}`);
22339
24558
  return;
22340
- } catch (cmdError) {
22341
- throw new Error(`ZIP extraction failed via PowerShell and cmd/tar. PowerShell: ${String(powershellError)} | cmd/tar: ${String(cmdError)}`);
22342
24559
  }
24560
+ if (owningPid !== process.pid) {
24561
+ log(`[onnx] not releasing lock ${lockPath2}: owned by pid ${owningPid ?? "unknown"} (we are ${process.pid})`);
24562
+ return;
24563
+ }
24564
+ try {
24565
+ unlinkSync4(lockPath2);
24566
+ } catch (unlinkErr) {
24567
+ const code = unlinkErr.code;
24568
+ if (code !== "ENOENT") {
24569
+ warn(`[onnx] failed to release lock ${lockPath2}: ${unlinkErr}`);
24570
+ }
24571
+ }
24572
+ } catch (err) {
24573
+ warn(`[onnx] unexpected error releasing lock ${lockPath2}: ${err}`);
24574
+ }
24575
+ }
24576
+ function isProcessAlive2(pid) {
24577
+ try {
24578
+ process.kill(pid, 0);
24579
+ return true;
24580
+ } catch (err) {
24581
+ const code = err.code;
24582
+ if (code === "ESRCH")
24583
+ return false;
24584
+ return true;
22343
24585
  }
22344
- execFileSync("unzip", ["-q", archivePath, "-d", destinationDir], {
22345
- stdio: "pipe",
22346
- timeout: 120000
22347
- });
22348
24586
  }
22349
24587
 
22350
24588
  // src/bridge.ts
22351
- import { spawn } from "child_process";
22352
- import { homedir as homedir4 } from "os";
22353
- import { join as join6 } from "path";
24589
+ import { spawn as spawn3 } from "child_process";
24590
+ import { homedir as homedir7 } from "os";
24591
+ import { join as join12 } from "path";
22354
24592
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
22355
24593
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
22356
24594
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
@@ -22492,14 +24730,14 @@ class BinaryBridge {
22492
24730
  const line = `${JSON.stringify(request)}
22493
24731
  `;
22494
24732
  const effectiveTimeoutMs = options?.timeoutMs ?? this.timeoutMs;
22495
- return new Promise((resolve, reject) => {
24733
+ return new Promise((resolve4, reject) => {
22496
24734
  const timer = setTimeout(() => {
22497
24735
  this.pending.delete(id);
22498
24736
  warn(`Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms \u2014 restarting bridge`);
22499
24737
  reject(new Error(`[aft-plugin] Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
22500
24738
  this.handleTimeout();
22501
24739
  }, effectiveTimeoutMs);
22502
- this.pending.set(id, { resolve, reject, timer });
24740
+ this.pending.set(id, { resolve: resolve4, reject, timer });
22503
24741
  if (!this.process?.stdin?.writable) {
22504
24742
  this.pending.delete(id);
22505
24743
  clearTimeout(timer);
@@ -22542,15 +24780,15 @@ class BinaryBridge {
22542
24780
  if (this.process) {
22543
24781
  const proc = this.process;
22544
24782
  this.process = null;
22545
- return new Promise((resolve) => {
24783
+ return new Promise((resolve4) => {
22546
24784
  const forceKillTimer = setTimeout(() => {
22547
24785
  proc.kill("SIGKILL");
22548
- resolve();
24786
+ resolve4();
22549
24787
  }, 5000);
22550
24788
  proc.once("exit", () => {
22551
24789
  clearTimeout(forceKillTimer);
22552
24790
  log("Process exited during shutdown");
22553
- resolve();
24791
+ resolve4();
22554
24792
  });
22555
24793
  proc.kill("SIGTERM");
22556
24794
  });
@@ -22592,19 +24830,19 @@ class BinaryBridge {
22592
24830
  })();
22593
24831
  const useFastembedBackend = semanticBackend === undefined || semanticBackend === "fastembed" || semanticBackend === "";
22594
24832
  const ortDir = typeof this.configOverrides._ort_dylib_dir === "string" && useFastembedBackend ? this.configOverrides._ort_dylib_dir : null;
22595
- const ortLibraryPath = ortDir == null ? null : join6(ortDir, process.platform === "win32" ? "onnxruntime.dll" : process.platform === "darwin" ? "libonnxruntime.dylib" : "libonnxruntime.so");
24833
+ const ortLibraryPath = ortDir == null ? null : join12(ortDir, process.platform === "win32" ? "onnxruntime.dll" : process.platform === "darwin" ? "libonnxruntime.dylib" : "libonnxruntime.so");
22596
24834
  const envPath = process.platform === "win32" && ortDir ? `${ortDir};${process.env.PATH ?? ""}` : process.env.PATH;
22597
24835
  const env = {
22598
24836
  ...process.env,
22599
24837
  ...envPath ? { PATH: envPath } : {}
22600
24838
  };
22601
24839
  if (useFastembedBackend) {
22602
- env.FASTEMBED_CACHE_DIR = process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join6(this.configOverrides.storage_dir, "semantic", "models") : join6(homedir4() || "", ".cache", "fastembed"));
24840
+ env.FASTEMBED_CACHE_DIR = process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join12(this.configOverrides.storage_dir, "semantic", "models") : join12(homedir7() || "", ".cache", "fastembed"));
22603
24841
  if (ortLibraryPath) {
22604
24842
  env.ORT_DYLIB_PATH = ortLibraryPath;
22605
24843
  }
22606
24844
  }
22607
- const child = spawn(this.binaryPath, [], {
24845
+ const child = spawn3(this.binaryPath, [], {
22608
24846
  cwd: this.cwd,
22609
24847
  stdio: ["pipe", "pipe", "pipe"],
22610
24848
  env
@@ -22865,10 +25103,10 @@ function normalizeKey(projectRoot) {
22865
25103
 
22866
25104
  // src/resolver.ts
22867
25105
  import { execSync, spawnSync } from "child_process";
22868
- import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync } from "fs";
25106
+ import { chmodSync as chmodSync3, copyFileSync as copyFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6, renameSync as renameSync2 } from "fs";
22869
25107
  import { createRequire } from "module";
22870
- import { homedir as homedir5 } from "os";
22871
- import { join as join7 } from "path";
25108
+ import { homedir as homedir8 } from "os";
25109
+ import { join as join13 } from "path";
22872
25110
  function copyToVersionedCache(npmBinaryPath) {
22873
25111
  try {
22874
25112
  const result = spawnSync(npmBinaryPath, ["--version"], {
@@ -22882,18 +25120,18 @@ function copyToVersionedCache(npmBinaryPath) {
22882
25120
  const version2 = rawVersion.replace(/^aft\s+/, "");
22883
25121
  const tag = version2.startsWith("v") ? version2 : `v${version2}`;
22884
25122
  const cacheDir = getCacheDir();
22885
- const versionedDir = join7(cacheDir, tag);
25123
+ const versionedDir = join13(cacheDir, tag);
22886
25124
  const ext = process.platform === "win32" ? ".exe" : "";
22887
- const cachedPath = join7(versionedDir, `aft${ext}`);
22888
- if (existsSync5(cachedPath))
25125
+ const cachedPath = join13(versionedDir, `aft${ext}`);
25126
+ if (existsSync9(cachedPath))
22889
25127
  return cachedPath;
22890
- mkdirSync4(versionedDir, { recursive: true });
25128
+ mkdirSync6(versionedDir, { recursive: true });
22891
25129
  const tmpPath = `${cachedPath}.tmp`;
22892
- copyFileSync(npmBinaryPath, tmpPath);
25130
+ copyFileSync3(npmBinaryPath, tmpPath);
22893
25131
  if (process.platform !== "win32") {
22894
25132
  chmodSync3(tmpPath, 493);
22895
25133
  }
22896
- renameSync(tmpPath, cachedPath);
25134
+ renameSync2(tmpPath, cachedPath);
22897
25135
  log(`Copied npm binary to versioned cache: ${cachedPath}`);
22898
25136
  return cachedPath;
22899
25137
  } catch (err) {
@@ -22901,14 +25139,14 @@ function copyToVersionedCache(npmBinaryPath) {
22901
25139
  return null;
22902
25140
  }
22903
25141
  }
22904
- function platformKey(platform2 = process.platform, arch = process.arch) {
22905
- const archMap = PLATFORM_ARCH_MAP[platform2];
25142
+ function platformKey(platform3 = process.platform, arch = process.arch) {
25143
+ const archMap = PLATFORM_ARCH_MAP[platform3];
22906
25144
  if (!archMap) {
22907
- throw new Error(`Unsupported platform: ${platform2} (arch: ${arch}). ` + `Supported platforms: ${Object.keys(PLATFORM_ARCH_MAP).join(", ")}`);
25145
+ throw new Error(`Unsupported platform: ${platform3} (arch: ${arch}). ` + `Supported platforms: ${Object.keys(PLATFORM_ARCH_MAP).join(", ")}`);
22908
25146
  }
22909
25147
  const key = archMap[arch];
22910
25148
  if (!key) {
22911
- throw new Error(`Unsupported architecture: ${arch} on platform ${platform2}. ` + `Supported architectures for ${platform2}: ${Object.keys(archMap).join(", ")}`);
25149
+ throw new Error(`Unsupported architecture: ${arch} on platform ${platform3}. ` + `Supported architectures for ${platform3}: ${Object.keys(archMap).join(", ")}`);
22912
25150
  }
22913
25151
  return key;
22914
25152
  }
@@ -22932,7 +25170,7 @@ function findBinarySync() {
22932
25170
  const packageBin = `@cortexkit/aft-${key}/bin/aft${ext}`;
22933
25171
  const req = createRequire(import.meta.url);
22934
25172
  const resolved = req.resolve(packageBin);
22935
- if (existsSync5(resolved)) {
25173
+ if (existsSync9(resolved)) {
22936
25174
  const copied = copyToVersionedCache(resolved);
22937
25175
  return copied ?? resolved;
22938
25176
  }
@@ -22946,8 +25184,8 @@ function findBinarySync() {
22946
25184
  if (result)
22947
25185
  return result;
22948
25186
  } catch {}
22949
- const cargoPath = join7(homedir5(), ".cargo", "bin", `aft${ext}`);
22950
- if (existsSync5(cargoPath))
25187
+ const cargoPath = join13(homedir8(), ".cargo", "bin", `aft${ext}`);
25188
+ if (existsSync9(cargoPath))
22951
25189
  return cargoPath;
22952
25190
  return null;
22953
25191
  }
@@ -22982,21 +25220,21 @@ async function findBinary() {
22982
25220
  }
22983
25221
 
22984
25222
  // src/shared/rpc-server.ts
22985
- import { randomBytes } from "crypto";
22986
- import { mkdirSync as mkdirSync5, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "fs";
25223
+ import { randomBytes as randomBytes2 } from "crypto";
25224
+ import { mkdirSync as mkdirSync7, renameSync as renameSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
22987
25225
  import { createServer } from "http";
22988
- import { dirname } from "path";
25226
+ import { dirname as dirname5 } from "path";
22989
25227
 
22990
25228
  // src/shared/rpc-utils.ts
22991
- import { createHash } from "crypto";
22992
- import { join as join8 } from "path";
25229
+ import { createHash as createHash4 } from "crypto";
25230
+ import { join as join14 } from "path";
22993
25231
  function projectHash(directory) {
22994
25232
  const normalized = directory.replace(/\/+$/, "");
22995
- return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
25233
+ return createHash4("sha256").update(normalized).digest("hex").slice(0, 16);
22996
25234
  }
22997
25235
  function rpcPortFilePath(storageDir, directory) {
22998
25236
  const hash2 = projectHash(directory);
22999
- return join8(storageDir, "rpc", hash2, "port");
25237
+ return join14(storageDir, "rpc", hash2, "port");
23000
25238
  }
23001
25239
 
23002
25240
  // src/shared/rpc-server.ts
@@ -23013,7 +25251,7 @@ class AftRpcServer {
23013
25251
  this.handlers.set(method, handler);
23014
25252
  }
23015
25253
  async start() {
23016
- return new Promise((resolve, reject) => {
25254
+ return new Promise((resolve4, reject) => {
23017
25255
  const server = createServer((req, res) => this.dispatch(req, res));
23018
25256
  server.on("error", (err) => {
23019
25257
  warn(`RPC server error: ${err.message}`);
@@ -23026,19 +25264,19 @@ class AftRpcServer {
23026
25264
  return;
23027
25265
  }
23028
25266
  this.port = addr.port;
23029
- this.token = randomBytes(32).toString("hex");
25267
+ this.token = randomBytes2(32).toString("hex");
23030
25268
  this.server = server;
23031
25269
  try {
23032
- const dir = dirname(this.portFilePath);
23033
- mkdirSync5(dir, { recursive: true });
25270
+ const dir = dirname5(this.portFilePath);
25271
+ mkdirSync7(dir, { recursive: true });
23034
25272
  const tmpPath = `${this.portFilePath}.tmp`;
23035
- writeFileSync2(tmpPath, JSON.stringify({ port: this.port, token: this.token }), "utf-8");
23036
- renameSync2(tmpPath, this.portFilePath);
25273
+ writeFileSync6(tmpPath, JSON.stringify({ port: this.port, token: this.token }), "utf-8");
25274
+ renameSync3(tmpPath, this.portFilePath);
23037
25275
  log(`RPC server listening on 127.0.0.1:${this.port}`);
23038
25276
  } catch (err) {
23039
25277
  warn(`Failed to write RPC port file: ${err}`);
23040
25278
  }
23041
- resolve(this.port);
25279
+ resolve4(this.port);
23042
25280
  });
23043
25281
  server.unref();
23044
25282
  });
@@ -23050,7 +25288,7 @@ class AftRpcServer {
23050
25288
  }
23051
25289
  this.token = null;
23052
25290
  try {
23053
- unlinkSync3(this.portFilePath);
25291
+ unlinkSync5(this.portFilePath);
23054
25292
  } catch {}
23055
25293
  }
23056
25294
  dispatch(req, res) {
@@ -23271,22 +25509,22 @@ function formatStatusMarkdown(status) {
23271
25509
  }
23272
25510
 
23273
25511
  // src/shared/tui-config.ts
23274
- var import_comment_json2 = __toESM(require_src2(), 1);
23275
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
23276
- import { dirname as dirname2, join as join10 } from "path";
25512
+ var import_comment_json4 = __toESM(require_src2(), 1);
25513
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
25514
+ import { dirname as dirname6, join as join16 } from "path";
23277
25515
 
23278
25516
  // src/shared/opencode-config-dir.ts
23279
- import { homedir as homedir6 } from "os";
23280
- import { join as join9, resolve } from "path";
25517
+ import { homedir as homedir9 } from "os";
25518
+ import { join as join15, resolve as resolve4 } from "path";
23281
25519
  function getCliConfigDir() {
23282
25520
  const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
23283
25521
  if (envConfigDir) {
23284
- return resolve(envConfigDir);
25522
+ return resolve4(envConfigDir);
23285
25523
  }
23286
25524
  if (process.platform === "win32") {
23287
- return join9(homedir6(), ".config", "opencode");
25525
+ return join15(homedir9(), ".config", "opencode");
23288
25526
  }
23289
- return join9(process.env.XDG_CONFIG_HOME || join9(homedir6(), ".config"), "opencode");
25527
+ return join15(process.env.XDG_CONFIG_HOME || join15(homedir9(), ".config"), "opencode");
23290
25528
  }
23291
25529
  function getOpenCodeConfigDir2(_options) {
23292
25530
  return getCliConfigDir();
@@ -23295,10 +25533,10 @@ function getOpenCodeConfigPaths(options) {
23295
25533
  const configDir = getOpenCodeConfigDir2(options);
23296
25534
  return {
23297
25535
  configDir,
23298
- configJson: join9(configDir, "opencode.json"),
23299
- configJsonc: join9(configDir, "opencode.jsonc"),
23300
- packageJson: join9(configDir, "package.json"),
23301
- omoConfig: join9(configDir, "magic-context.jsonc")
25536
+ configJson: join15(configDir, "opencode.json"),
25537
+ configJsonc: join15(configDir, "opencode.jsonc"),
25538
+ packageJson: join15(configDir, "package.json"),
25539
+ omoConfig: join15(configDir, "magic-context.jsonc")
23302
25540
  };
23303
25541
  }
23304
25542
 
@@ -23307,11 +25545,11 @@ var PLUGIN_NAME = "@cortexkit/aft-opencode";
23307
25545
  var PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
23308
25546
  function resolveTuiConfigPath() {
23309
25547
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
23310
- const jsoncPath = join10(configDir, "tui.jsonc");
23311
- const jsonPath = join10(configDir, "tui.json");
23312
- if (existsSync6(jsoncPath))
25548
+ const jsoncPath = join16(configDir, "tui.jsonc");
25549
+ const jsonPath = join16(configDir, "tui.json");
25550
+ if (existsSync10(jsoncPath))
23313
25551
  return jsoncPath;
23314
- if (existsSync6(jsonPath))
25552
+ if (existsSync10(jsonPath))
23315
25553
  return jsonPath;
23316
25554
  return jsonPath;
23317
25555
  }
@@ -23319,8 +25557,8 @@ function ensureTuiPluginEntry() {
23319
25557
  try {
23320
25558
  const configPath = resolveTuiConfigPath();
23321
25559
  let config2 = {};
23322
- if (existsSync6(configPath)) {
23323
- config2 = import_comment_json2.parse(readFileSync3(configPath, "utf-8")) ?? {};
25560
+ if (existsSync10(configPath)) {
25561
+ config2 = import_comment_json4.parse(readFileSync7(configPath, "utf-8")) ?? {};
23324
25562
  }
23325
25563
  const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
23326
25564
  if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) || plugin.includes("opencode-plugin") || plugin.includes("aft-opencode"))) {
@@ -23328,8 +25566,8 @@ function ensureTuiPluginEntry() {
23328
25566
  }
23329
25567
  plugins.push(PLUGIN_ENTRY);
23330
25568
  config2.plugin = plugins;
23331
- mkdirSync6(dirname2(configPath), { recursive: true });
23332
- writeFileSync3(configPath, `${import_comment_json2.stringify(config2, null, 2)}
25569
+ mkdirSync8(dirname6(configPath), { recursive: true });
25570
+ writeFileSync7(configPath, `${import_comment_json4.stringify(config2, null, 2)}
23333
25571
  `);
23334
25572
  log(`[aft-plugin] added TUI plugin entry to ${configPath}`);
23335
25573
  return true;
@@ -23340,33 +25578,33 @@ function ensureTuiPluginEntry() {
23340
25578
  }
23341
25579
 
23342
25580
  // src/shared/url-fetch.ts
23343
- import { createHash as createHash2 } from "crypto";
25581
+ import { createHash as createHash5 } from "crypto";
23344
25582
  import { lookup } from "dns/promises";
23345
25583
  import {
23346
- existsSync as existsSync7,
23347
- mkdirSync as mkdirSync7,
23348
- readdirSync as readdirSync2,
23349
- readFileSync as readFileSync4,
23350
- unlinkSync as unlinkSync4,
23351
- writeFileSync as writeFileSync4
25584
+ existsSync as existsSync11,
25585
+ mkdirSync as mkdirSync9,
25586
+ readdirSync as readdirSync4,
25587
+ readFileSync as readFileSync8,
25588
+ unlinkSync as unlinkSync6,
25589
+ writeFileSync as writeFileSync8
23352
25590
  } from "fs";
23353
25591
  import { isIP } from "net";
23354
- import { join as join11 } from "path";
25592
+ import { join as join17 } from "path";
23355
25593
  var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
23356
25594
  var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
23357
25595
  var FETCH_TIMEOUT_MS = 30000;
23358
25596
  var MAX_REDIRECTS = 5;
23359
25597
  function cacheDir(storageDir) {
23360
- return join11(storageDir, "url_cache");
25598
+ return join17(storageDir, "url_cache");
23361
25599
  }
23362
25600
  function hashUrl(url2) {
23363
- return createHash2("sha256").update(url2).digest("hex").slice(0, 16);
25601
+ return createHash5("sha256").update(url2).digest("hex").slice(0, 16);
23364
25602
  }
23365
25603
  function metaPath(storageDir, hash2) {
23366
- return join11(cacheDir(storageDir), `${hash2}.meta.json`);
25604
+ return join17(cacheDir(storageDir), `${hash2}.meta.json`);
23367
25605
  }
23368
25606
  function contentPath(storageDir, hash2, extension) {
23369
- return join11(cacheDir(storageDir), `${hash2}${extension}`);
25607
+ return join17(cacheDir(storageDir), `${hash2}${extension}`);
23370
25608
  }
23371
25609
  function _isPrivateIpv4(address) {
23372
25610
  const parts = address.split(".").map((part) => Number.parseInt(part, 10));
@@ -23525,15 +25763,15 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
23525
25763
  }
23526
25764
  const allowPrivate = options.allowPrivate === true;
23527
25765
  const dir = cacheDir(storageDir);
23528
- mkdirSync7(dir, { recursive: true });
25766
+ mkdirSync9(dir, { recursive: true });
23529
25767
  const hash2 = hashUrl(url2);
23530
25768
  const metaFile = metaPath(storageDir, hash2);
23531
- if (existsSync7(metaFile)) {
25769
+ if (existsSync11(metaFile)) {
23532
25770
  try {
23533
- const meta4 = JSON.parse(readFileSync4(metaFile, "utf8"));
25771
+ const meta4 = JSON.parse(readFileSync8(metaFile, "utf8"));
23534
25772
  const age = Date.now() - meta4.fetchedAt;
23535
25773
  const cached2 = contentPath(storageDir, hash2, meta4.extension);
23536
- if (age < CACHE_TTL_MS && existsSync7(cached2)) {
25774
+ if (age < CACHE_TTL_MS && existsSync11(cached2)) {
23537
25775
  log(`URL cache hit: ${url2} (${Math.round(age / 1000)}s old)`);
23538
25776
  return cached2;
23539
25777
  }
@@ -23578,9 +25816,9 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
23578
25816
  const body = Buffer.concat(chunks);
23579
25817
  const contentFile = contentPath(storageDir, hash2, extension);
23580
25818
  const tmpContent = `${contentFile}.tmp-${process.pid}`;
23581
- writeFileSync4(tmpContent, body);
23582
- const { renameSync: renameSync3 } = await import("fs");
23583
- renameSync3(tmpContent, contentFile);
25819
+ writeFileSync8(tmpContent, body);
25820
+ const { renameSync: renameSync4 } = await import("fs");
25821
+ renameSync4(tmpContent, contentFile);
23584
25822
  const meta3 = {
23585
25823
  url: url2,
23586
25824
  contentType,
@@ -23588,35 +25826,35 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
23588
25826
  fetchedAt: Date.now()
23589
25827
  };
23590
25828
  const tmpMeta = `${metaFile}.tmp-${process.pid}`;
23591
- writeFileSync4(tmpMeta, JSON.stringify(meta3));
23592
- renameSync3(tmpMeta, metaFile);
25829
+ writeFileSync8(tmpMeta, JSON.stringify(meta3));
25830
+ renameSync4(tmpMeta, metaFile);
23593
25831
  log(`URL cached (${total} bytes): ${url2}`);
23594
25832
  return contentFile;
23595
25833
  }
23596
25834
  function cleanupUrlCache(storageDir) {
23597
25835
  const dir = cacheDir(storageDir);
23598
- if (!existsSync7(dir))
25836
+ if (!existsSync11(dir))
23599
25837
  return;
23600
25838
  let removed = 0;
23601
25839
  try {
23602
- for (const entry of readdirSync2(dir)) {
25840
+ for (const entry of readdirSync4(dir)) {
23603
25841
  if (!entry.endsWith(".meta.json"))
23604
25842
  continue;
23605
- const metaFile = join11(dir, entry);
25843
+ const metaFile = join17(dir, entry);
23606
25844
  try {
23607
- const meta3 = JSON.parse(readFileSync4(metaFile, "utf8"));
25845
+ const meta3 = JSON.parse(readFileSync8(metaFile, "utf8"));
23608
25846
  const age = Date.now() - meta3.fetchedAt;
23609
25847
  if (age > CACHE_TTL_MS) {
23610
25848
  const hash2 = entry.slice(0, -".meta.json".length);
23611
25849
  const content = contentPath(storageDir, hash2, meta3.extension);
23612
- if (existsSync7(content))
23613
- unlinkSync4(content);
23614
- unlinkSync4(metaFile);
25850
+ if (existsSync11(content))
25851
+ unlinkSync6(content);
25852
+ unlinkSync6(metaFile);
23615
25853
  removed++;
23616
25854
  }
23617
25855
  } catch {
23618
25856
  try {
23619
- unlinkSync4(metaFile);
25857
+ unlinkSync6(metaFile);
23620
25858
  removed++;
23621
25859
  } catch {}
23622
25860
  }
@@ -25291,7 +27529,8 @@ function lspTools(ctx) {
25291
27529
  ` + `}
25292
27530
  ` + `
25293
27531
  ` + `**Reading the response honestly:**
25294
- ` + "- `total: 0, complete: true, lsp_servers_used: [{status: 'pull_ok'}]` \u2192 file is genuinely clean.\n" + "- `total: 0, lsp_servers_used: []` \u2192 no server registered for this extension; nothing was checked.\n" + "- `lsp_servers_used: [{status: 'binary_not_installed: bash-language-server'}]` \u2192 install the server to get diagnostics.\n" + "- `complete: false` (directory mode) \u2192 some files in the directory weren't checked; see `unchecked_files`.",
27532
+ ` + "- `total: 0, complete: true, lsp_servers_used: [{status: 'pull_ok'}]` \u2192 file is genuinely clean.\n" + "- `total: 0, lsp_servers_used: []` \u2192 **nothing was checked** (no server registered for this extension). Tell the user, don't claim 'no errors'.\n" + "- `lsp_servers_used: [{status: 'binary_not_installed: <name>'}]` \u2192 server matched the extension but its binary isn't on PATH. Tell the user to install it.\n" + "- `lsp_servers_used: [{status: 'no_root_marker (...)'}]` \u2192 server is registered but couldn't find a workspace root marker walking up from this file. The user's project layout doesn't match what the server expects.\n" + "- `complete: false` (directory mode) \u2192 some files in the directory weren't checked; see `unchecked_files`.\n" + `
27533
+ ` + "**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).",
25295
27534
  args: {
25296
27535
  filePath: z5.string().optional().describe("Path to a file to check. Mutually exclusive with 'directory'. Omit both to dump all cached diagnostics."),
25297
27536
  directory: z5.string().optional().describe("Path to a directory. Returns cached diagnostics + workspace pull from active servers; lists files we have no info for in 'unchecked_files'. Capped at 200 walked files."),
@@ -25375,7 +27614,7 @@ function navigationTools(ctx) {
25375
27614
 
25376
27615
  // src/tools/reading.ts
25377
27616
  import { readdir } from "fs/promises";
25378
- import { extname as extname2, join as join12, resolve as resolve5 } from "path";
27617
+ import { extname as extname2, join as join18, resolve as resolve8 } from "path";
25379
27618
  import { tool as tool7 } from "@opencode-ai/plugin";
25380
27619
  var OUTLINE_EXTENSIONS = new Set([
25381
27620
  ".ts",
@@ -25453,7 +27692,7 @@ function readingTools(ctx) {
25453
27692
  if (!dirArg && typeof args.filePath === "string" && !Array.isArray(args.files)) {
25454
27693
  try {
25455
27694
  const { stat } = await import("fs/promises");
25456
- const resolved = resolve5(context.directory, args.filePath);
27695
+ const resolved = resolve8(context.directory, args.filePath);
25457
27696
  const st = await stat(resolved);
25458
27697
  if (st.isDirectory()) {
25459
27698
  dirArg = args.filePath;
@@ -25461,7 +27700,7 @@ function readingTools(ctx) {
25461
27700
  } catch {}
25462
27701
  }
25463
27702
  if (dirArg) {
25464
- const dirPath = resolve5(context.directory, dirArg);
27703
+ const dirPath = resolve8(context.directory, dirArg);
25465
27704
  const files = await discoverSourceFiles(dirPath);
25466
27705
  if (files.length === 0) {
25467
27706
  return JSON.stringify({
@@ -25569,12 +27808,12 @@ async function discoverSourceFiles(dir, maxFiles = 200) {
25569
27808
  return;
25570
27809
  if (entry.isDirectory()) {
25571
27810
  if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
25572
- await walk(join12(current, entry.name));
27811
+ await walk(join18(current, entry.name));
25573
27812
  }
25574
27813
  } else if (entry.isFile()) {
25575
27814
  const ext = extname2(entry.name).toLowerCase();
25576
27815
  if (OUTLINE_EXTENSIONS.has(ext)) {
25577
- files.push(join12(current, entry.name));
27816
+ files.push(join18(current, entry.name));
25578
27817
  }
25579
27818
  }
25580
27819
  }
@@ -26147,6 +28386,7 @@ var ANNOUNCEMENT_FEATURES = [];
26147
28386
  var plugin = async (input) => {
26148
28387
  const binaryPath = await findBinary();
26149
28388
  const aftConfig = loadAftConfig(input.directory);
28389
+ const autoUpdateAbort = new AbortController;
26150
28390
  const configOverrides = {};
26151
28391
  if (aftConfig.format_on_edit !== undefined)
26152
28392
  configOverrides.format_on_edit = aftConfig.format_on_edit;
@@ -26167,8 +28407,8 @@ var plugin = async (input) => {
26167
28407
  if (aftConfig.max_callgraph_files !== undefined)
26168
28408
  configOverrides.max_callgraph_files = aftConfig.max_callgraph_files;
26169
28409
  const isFastembedSemanticBackend = (aftConfig.semantic?.backend ?? "fastembed") === "fastembed";
26170
- const dataHome = process.env.XDG_DATA_HOME || join13(homedir7(), ".local", "share");
26171
- configOverrides.storage_dir = join13(dataHome, "opencode", "storage", "plugin", "aft");
28410
+ const dataHome = process.env.XDG_DATA_HOME || join19(homedir10(), ".local", "share");
28411
+ configOverrides.storage_dir = join19(dataHome, "opencode", "storage", "plugin", "aft");
26172
28412
  if (aftConfig.experimental_semantic_search && isFastembedSemanticBackend) {
26173
28413
  const storageDir2 = configOverrides.storage_dir;
26174
28414
  const ortDylibDir = await ensureOnnxRuntime(storageDir2).catch((err) => {
@@ -26181,6 +28421,63 @@ var plugin = async (input) => {
26181
28421
  warn(`Semantic search requires ONNX Runtime. Install: ${getManualInstallHint()}`);
26182
28422
  }
26183
28423
  }
28424
+ try {
28425
+ const lspAutoInstall = aftConfig.lsp?.auto_install ?? true;
28426
+ const lspGraceDays = aftConfig.lsp?.grace_days ?? 7;
28427
+ const lspVersions = aftConfig.lsp?.versions ?? {};
28428
+ const lspDisabled = new Set(aftConfig.lsp?.disabled ?? []);
28429
+ const npmResult = runAutoInstall(input.directory, {
28430
+ autoInstall: lspAutoInstall,
28431
+ graceDays: lspGraceDays,
28432
+ versions: lspVersions,
28433
+ disabled: lspDisabled
28434
+ });
28435
+ const relevantGithub = discoverRelevantGithubServers(input.directory);
28436
+ const ghResult = runGithubAutoInstall(relevantGithub, {
28437
+ autoInstall: lspAutoInstall,
28438
+ graceDays: lspGraceDays,
28439
+ versions: lspVersions,
28440
+ disabled: lspDisabled
28441
+ });
28442
+ const mergedBinDirs = [...npmResult.cachedBinDirs, ...ghResult.cachedBinDirs];
28443
+ if (mergedBinDirs.length > 0) {
28444
+ configOverrides.lsp_paths_extra = mergedBinDirs;
28445
+ }
28446
+ if (npmResult.installsStarted > 0 || ghResult.installsStarted > 0) {
28447
+ log(`[lsp] auto-install: ${npmResult.installsStarted} npm + ${ghResult.installsStarted} github install(s) running in background`);
28448
+ }
28449
+ Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
28450
+ const actionable = [...npmResult.skipped, ...ghResult.skipped].filter((s) => {
28451
+ const r = s.reason.toLowerCase();
28452
+ if (r === "auto_install: false")
28453
+ return false;
28454
+ if (r === "disabled by config")
28455
+ return false;
28456
+ if (r === "not relevant to project")
28457
+ return false;
28458
+ if (r === "already installed")
28459
+ return false;
28460
+ if (r === "another install in progress")
28461
+ return false;
28462
+ return true;
28463
+ });
28464
+ if (actionable.length === 0)
28465
+ return;
28466
+ const lines = actionable.map((s) => ` \u2022 ${s.id}: ${s.reason}`).join(`
28467
+ `);
28468
+ const message = `AFT skipped or failed to install ${actionable.length} LSP server(s):
28469
+ ${lines}
28470
+
28471
+ ` + "See `/aft-status` for details, or check the plugin log. " + 'Pin a working version with `lsp.versions: { "<package>": "<version>" }` if grace is blocking, ' + "or set `lsp.auto_install: false` to suppress this entirely.";
28472
+ sendWarning({ client: input.client, directory: input.directory }, message).catch((err) => {
28473
+ warn(`[lsp] failed to deliver install summary: ${err}`);
28474
+ });
28475
+ }).catch((err) => {
28476
+ warn(`[lsp] install-summary aggregation failed: ${err}`);
28477
+ });
28478
+ } catch (err) {
28479
+ warn(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
28480
+ }
26184
28481
  let versionUpgradeAttempted = null;
26185
28482
  const pool = new BridgePool(binaryPath, {
26186
28483
  minVersion: PLUGIN_VERSION,
@@ -26228,6 +28525,7 @@ var plugin = async (input) => {
26228
28525
  setSharedBridgePool(pool);
26229
28526
  const rpcServer = new AftRpcServer(configOverrides.storage_dir, input.directory);
26230
28527
  const unregisterShutdown = registerShutdownCleanup(async () => {
28528
+ await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
26231
28529
  try {
26232
28530
  rpcServer.stop();
26233
28531
  } catch {}
@@ -26244,10 +28542,10 @@ var plugin = async (input) => {
26244
28542
  return { show: false };
26245
28543
  }
26246
28544
  if (storageDir) {
26247
- const versionFile = join13(storageDir, "last_announced_version");
28545
+ const versionFile = join19(storageDir, "last_announced_version");
26248
28546
  try {
26249
- if (existsSync9(versionFile)) {
26250
- const lastVersion = readFileSync5(versionFile, "utf-8").trim();
28547
+ if (existsSync13(versionFile)) {
28548
+ const lastVersion = readFileSync9(versionFile, "utf-8").trim();
26251
28549
  if (lastVersion === ANNOUNCEMENT_VERSION)
26252
28550
  return { show: false };
26253
28551
  }
@@ -26258,8 +28556,8 @@ var plugin = async (input) => {
26258
28556
  rpcServer.handle("mark-announced", async () => {
26259
28557
  if (storageDir && ANNOUNCEMENT_VERSION) {
26260
28558
  try {
26261
- mkdirSync8(storageDir, { recursive: true });
26262
- writeFileSync5(join13(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
28559
+ mkdirSync10(storageDir, { recursive: true });
28560
+ writeFileSync9(join19(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
26263
28561
  } catch {}
26264
28562
  }
26265
28563
  return { success: true };
@@ -26342,6 +28640,11 @@ Install: ${getManualInstallHint()}`).catch(() => {});
26342
28640
  }
26343
28641
  return {
26344
28642
  tool: allTools,
28643
+ event: createAutoUpdateCheckerHook(input, {
28644
+ enabled: true,
28645
+ autoUpdate: aftConfig.auto_update ?? true,
28646
+ signal: autoUpdateAbort.signal
28647
+ }),
26345
28648
  "command.execute.before": async (commandInput, _output) => {
26346
28649
  if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
26347
28650
  return;
@@ -26389,11 +28692,13 @@ Install: ${getManualInstallHint()}`).catch(() => {});
26389
28692
  }
26390
28693
  };
26391
28694
  },
26392
- dispose: () => {
28695
+ dispose: async () => {
28696
+ autoUpdateAbort.abort();
26393
28697
  unregisterShutdown();
28698
+ await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
26394
28699
  rpcServer.stop();
26395
28700
  clearSharedBridgePool();
26396
- return pool.shutdown();
28701
+ await pool.shutdown();
26397
28702
  }
26398
28703
  };
26399
28704
  };