@cortexkit/aft-opencode 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1649 -189
- package/dist/lsp-auto-install.d.ts +82 -0
- package/dist/lsp-auto-install.d.ts.map +1 -0
- package/dist/lsp-cache.d.ts +116 -0
- package/dist/lsp-cache.d.ts.map +1 -0
- package/dist/lsp-github-install.d.ts +90 -0
- package/dist/lsp-github-install.d.ts.map +1 -0
- package/dist/lsp-github-probe.d.ts +71 -0
- package/dist/lsp-github-probe.d.ts.map +1 -0
- package/dist/lsp-github-table.d.ts +62 -0
- package/dist/lsp-github-table.d.ts.map +1 -0
- package/dist/lsp-npm-table.d.ts +31 -0
- package/dist/lsp-npm-table.d.ts.map +1 -0
- package/dist/lsp-project-relevance.d.ts +11 -0
- package/dist/lsp-project-relevance.d.ts.map +1 -0
- package/dist/lsp-registry-probe.d.ts +50 -0
- package/dist/lsp-registry-probe.d.ts.map +1 -0
- package/dist/onnx-runtime.d.ts.map +1 -1
- 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
|
|
7806
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
7807
7807
|
import { createRequire as createRequire2 } from "module";
|
|
7808
|
-
import { homedir as
|
|
7809
|
-
import { join as
|
|
7808
|
+
import { homedir as homedir8 } from "os";
|
|
7809
|
+
import { join as join16 } 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(),
|
|
@@ -21490,7 +21493,7 @@ function resolveLspConfigForConfigure(config2) {
|
|
|
21490
21493
|
switch (config2.lsp?.python ?? "auto") {
|
|
21491
21494
|
case "ty":
|
|
21492
21495
|
experimentalTy = true;
|
|
21493
|
-
disabled.add("
|
|
21496
|
+
disabled.add("python");
|
|
21494
21497
|
break;
|
|
21495
21498
|
case "pyright":
|
|
21496
21499
|
experimentalTy = false;
|
|
@@ -21604,26 +21607,79 @@ function mergeSemanticConfig(baseSemantic, overrideSemantic) {
|
|
|
21604
21607
|
}
|
|
21605
21608
|
return Object.fromEntries(Object.entries(semantic).filter(([, value]) => value !== undefined));
|
|
21606
21609
|
}
|
|
21610
|
+
function mergeLspConfig(baseLsp, overrideLsp) {
|
|
21611
|
+
const projectLsp = {};
|
|
21612
|
+
if (overrideLsp?.python !== undefined)
|
|
21613
|
+
projectLsp.python = overrideLsp.python;
|
|
21614
|
+
const userDisabled = baseLsp?.disabled ?? [];
|
|
21615
|
+
const lsp = {
|
|
21616
|
+
...baseLsp,
|
|
21617
|
+
...projectLsp,
|
|
21618
|
+
...userDisabled.length > 0 ? { disabled: [...userDisabled] } : {}
|
|
21619
|
+
};
|
|
21620
|
+
if (Object.values(lsp).every((value) => value === undefined)) {
|
|
21621
|
+
return;
|
|
21622
|
+
}
|
|
21623
|
+
return Object.fromEntries(Object.entries(lsp).filter(([, value]) => value !== undefined));
|
|
21624
|
+
}
|
|
21625
|
+
function getProjectLspStrippedKeys(lsp) {
|
|
21626
|
+
if (!lsp) {
|
|
21627
|
+
return [];
|
|
21628
|
+
}
|
|
21629
|
+
const strippedKeys = [];
|
|
21630
|
+
if (lsp.servers !== undefined)
|
|
21631
|
+
strippedKeys.push("lsp.servers");
|
|
21632
|
+
if (lsp.versions !== undefined)
|
|
21633
|
+
strippedKeys.push("lsp.versions");
|
|
21634
|
+
if (lsp.auto_install !== undefined)
|
|
21635
|
+
strippedKeys.push("lsp.auto_install");
|
|
21636
|
+
if (lsp.grace_days !== undefined)
|
|
21637
|
+
strippedKeys.push("lsp.grace_days");
|
|
21638
|
+
if (lsp.disabled !== undefined)
|
|
21639
|
+
strippedKeys.push("lsp.disabled");
|
|
21640
|
+
return strippedKeys;
|
|
21641
|
+
}
|
|
21642
|
+
var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
|
|
21643
|
+
"tool_surface",
|
|
21644
|
+
"hoist_builtin_tools",
|
|
21645
|
+
"format_on_edit",
|
|
21646
|
+
"validate_on_edit",
|
|
21647
|
+
"experimental_search_index",
|
|
21648
|
+
"experimental_semantic_search",
|
|
21649
|
+
"experimental_lsp_ty"
|
|
21650
|
+
]);
|
|
21651
|
+
function pickProjectSafeFields(override) {
|
|
21652
|
+
const safe = {};
|
|
21653
|
+
for (const key of PROJECT_SAFE_TOP_LEVEL_FIELDS) {
|
|
21654
|
+
if (override[key] !== undefined) {
|
|
21655
|
+
safe[key] = override[key];
|
|
21656
|
+
}
|
|
21657
|
+
}
|
|
21658
|
+
return safe;
|
|
21659
|
+
}
|
|
21660
|
+
function getStrippedTopLevelKeys(override) {
|
|
21661
|
+
const stripped = [];
|
|
21662
|
+
if (override.restrict_to_project_root !== undefined)
|
|
21663
|
+
stripped.push("restrict_to_project_root");
|
|
21664
|
+
if (override.url_fetch_allow_private !== undefined)
|
|
21665
|
+
stripped.push("url_fetch_allow_private");
|
|
21666
|
+
if (override.max_callgraph_files !== undefined)
|
|
21667
|
+
stripped.push("max_callgraph_files");
|
|
21668
|
+
return stripped;
|
|
21669
|
+
}
|
|
21607
21670
|
function mergeConfigs(base, override) {
|
|
21608
21671
|
const disabledTools = [...base.disabled_tools ?? [], ...override.disabled_tools ?? []];
|
|
21609
21672
|
const formatter = { ...base.formatter, ...override.formatter };
|
|
21610
21673
|
const checker = { ...base.checker, ...override.checker };
|
|
21611
21674
|
const semantic = mergeSemanticConfig(base.semantic, override.semantic);
|
|
21612
|
-
const
|
|
21613
|
-
const
|
|
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;
|
|
21675
|
+
const lsp = mergeLspConfig(base.lsp, override.lsp);
|
|
21676
|
+
const safeOverride = pickProjectSafeFields(override);
|
|
21621
21677
|
return {
|
|
21622
21678
|
...base,
|
|
21623
21679
|
...safeOverride,
|
|
21624
21680
|
...Object.keys(formatter).length > 0 ? { formatter } : {},
|
|
21625
21681
|
...Object.keys(checker).length > 0 ? { checker } : {},
|
|
21626
|
-
...
|
|
21682
|
+
...lsp ? { lsp } : {},
|
|
21627
21683
|
semantic,
|
|
21628
21684
|
...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
|
|
21629
21685
|
};
|
|
@@ -21650,6 +21706,14 @@ function loadAftConfig(projectDirectory) {
|
|
|
21650
21706
|
if (projectConfig.semantic?.backend !== undefined || projectConfig.semantic?.base_url !== undefined || projectConfig.semantic?.api_key_env !== undefined) {
|
|
21651
21707
|
warn("Ignoring semantic.backend/base_url/api_key_env from project config (security: use user config for external backends)");
|
|
21652
21708
|
}
|
|
21709
|
+
const strippedLspKeys = getProjectLspStrippedKeys(projectConfig.lsp);
|
|
21710
|
+
if (strippedLspKeys.length > 0) {
|
|
21711
|
+
warn(`Ignoring ${strippedLspKeys.join(", ")} from project config ${projectConfigPath} (security: these LSP settings only honor user-level config)`);
|
|
21712
|
+
}
|
|
21713
|
+
const strippedTopLevelKeys = getStrippedTopLevelKeys(projectConfig);
|
|
21714
|
+
if (strippedTopLevelKeys.length > 0) {
|
|
21715
|
+
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)`);
|
|
21716
|
+
}
|
|
21653
21717
|
config2 = mergeConfigs(config2, projectConfig);
|
|
21654
21718
|
}
|
|
21655
21719
|
return config2;
|
|
@@ -21805,6 +21869,1361 @@ async function fetchLatestTag() {
|
|
|
21805
21869
|
}
|
|
21806
21870
|
}
|
|
21807
21871
|
|
|
21872
|
+
// src/lsp-auto-install.ts
|
|
21873
|
+
import { spawn } from "child_process";
|
|
21874
|
+
import { createHash } from "crypto";
|
|
21875
|
+
import { createReadStream, statSync as statSync2 } from "fs";
|
|
21876
|
+
|
|
21877
|
+
// src/lsp-cache.ts
|
|
21878
|
+
import {
|
|
21879
|
+
closeSync,
|
|
21880
|
+
existsSync as existsSync3,
|
|
21881
|
+
mkdirSync as mkdirSync2,
|
|
21882
|
+
openSync,
|
|
21883
|
+
readFileSync as readFileSync2,
|
|
21884
|
+
statSync,
|
|
21885
|
+
unlinkSync as unlinkSync2,
|
|
21886
|
+
writeFileSync
|
|
21887
|
+
} from "fs";
|
|
21888
|
+
import { homedir as homedir3 } from "os";
|
|
21889
|
+
import { join as join4 } from "path";
|
|
21890
|
+
function aftCacheBase() {
|
|
21891
|
+
const override = process.env.AFT_CACHE_DIR;
|
|
21892
|
+
if (override && override.length > 0)
|
|
21893
|
+
return override;
|
|
21894
|
+
if (process.platform === "win32") {
|
|
21895
|
+
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
21896
|
+
const base2 = localAppData || join4(homedir3(), "AppData", "Local");
|
|
21897
|
+
return join4(base2, "aft");
|
|
21898
|
+
}
|
|
21899
|
+
const base = process.env.XDG_CACHE_HOME || join4(homedir3(), ".cache");
|
|
21900
|
+
return join4(base, "aft");
|
|
21901
|
+
}
|
|
21902
|
+
function lspCacheRoot() {
|
|
21903
|
+
return join4(aftCacheBase(), "lsp-packages");
|
|
21904
|
+
}
|
|
21905
|
+
function lspPackageDir(npmPackage) {
|
|
21906
|
+
return join4(lspCacheRoot(), encodeURIComponent(npmPackage));
|
|
21907
|
+
}
|
|
21908
|
+
function lspBinaryPath(npmPackage, binary) {
|
|
21909
|
+
return join4(lspPackageDir(npmPackage), "node_modules", ".bin", binary);
|
|
21910
|
+
}
|
|
21911
|
+
function lspBinDir(npmPackage) {
|
|
21912
|
+
return join4(lspPackageDir(npmPackage), "node_modules", ".bin");
|
|
21913
|
+
}
|
|
21914
|
+
function isInstalled(npmPackage, binary) {
|
|
21915
|
+
for (const candidate of lspBinaryCandidates(binary)) {
|
|
21916
|
+
try {
|
|
21917
|
+
if (statSync(join4(lspBinDir(npmPackage), candidate)).isFile())
|
|
21918
|
+
return true;
|
|
21919
|
+
} catch {}
|
|
21920
|
+
}
|
|
21921
|
+
return false;
|
|
21922
|
+
}
|
|
21923
|
+
function lspBinaryCandidates(binary) {
|
|
21924
|
+
if (process.platform !== "win32")
|
|
21925
|
+
return [binary];
|
|
21926
|
+
return [binary, `${binary}.cmd`, `${binary}.exe`, `${binary}.bat`];
|
|
21927
|
+
}
|
|
21928
|
+
var INSTALLED_META_FILE = ".aft-installed";
|
|
21929
|
+
function writeInstalledMetaIn(installDir, version2, sha256) {
|
|
21930
|
+
try {
|
|
21931
|
+
mkdirSync2(installDir, { recursive: true });
|
|
21932
|
+
const meta3 = {
|
|
21933
|
+
version: version2,
|
|
21934
|
+
installedAt: new Date().toISOString(),
|
|
21935
|
+
...sha256 ? { sha256 } : {}
|
|
21936
|
+
};
|
|
21937
|
+
writeFileSync(join4(installDir, INSTALLED_META_FILE), JSON.stringify(meta3), "utf8");
|
|
21938
|
+
} catch (err) {
|
|
21939
|
+
log(`[lsp-cache] failed to write installed-meta in ${installDir}: ${err}`);
|
|
21940
|
+
}
|
|
21941
|
+
}
|
|
21942
|
+
function readInstalledMetaIn(installDir) {
|
|
21943
|
+
const path2 = join4(installDir, INSTALLED_META_FILE);
|
|
21944
|
+
try {
|
|
21945
|
+
if (!statSync(path2).isFile())
|
|
21946
|
+
return null;
|
|
21947
|
+
const raw = readFileSync2(path2, "utf8");
|
|
21948
|
+
const parsed = JSON.parse(raw);
|
|
21949
|
+
if (typeof parsed.version !== "string" || parsed.version.length === 0)
|
|
21950
|
+
return null;
|
|
21951
|
+
return {
|
|
21952
|
+
version: parsed.version,
|
|
21953
|
+
installedAt: typeof parsed.installedAt === "string" ? parsed.installedAt : "",
|
|
21954
|
+
...typeof parsed.sha256 === "string" && parsed.sha256.length > 0 ? { sha256: parsed.sha256 } : {}
|
|
21955
|
+
};
|
|
21956
|
+
} catch {
|
|
21957
|
+
return null;
|
|
21958
|
+
}
|
|
21959
|
+
}
|
|
21960
|
+
function writeInstalledMeta(packageKey, version2, sha256) {
|
|
21961
|
+
writeInstalledMetaIn(lspPackageDir(packageKey), version2, sha256);
|
|
21962
|
+
}
|
|
21963
|
+
function readInstalledMeta(packageKey) {
|
|
21964
|
+
return readInstalledMetaIn(lspPackageDir(packageKey));
|
|
21965
|
+
}
|
|
21966
|
+
function lockPath(npmPackage) {
|
|
21967
|
+
return join4(lspPackageDir(npmPackage), ".aft-installing");
|
|
21968
|
+
}
|
|
21969
|
+
var STALE_LOCK_MS = 30 * 60 * 1000;
|
|
21970
|
+
function acquireInstallLock(lockKey) {
|
|
21971
|
+
mkdirSync2(lspPackageDir(lockKey), { recursive: true });
|
|
21972
|
+
const lock = lockPath(lockKey);
|
|
21973
|
+
const tryClaim = () => {
|
|
21974
|
+
try {
|
|
21975
|
+
const fd = openSync(lock, "wx");
|
|
21976
|
+
try {
|
|
21977
|
+
writeFileSync(fd, `${process.pid}
|
|
21978
|
+
${new Date().toISOString()}
|
|
21979
|
+
`);
|
|
21980
|
+
} finally {
|
|
21981
|
+
closeSync(fd);
|
|
21982
|
+
}
|
|
21983
|
+
return true;
|
|
21984
|
+
} catch (err) {
|
|
21985
|
+
const code = err.code;
|
|
21986
|
+
if (code === "EEXIST")
|
|
21987
|
+
return false;
|
|
21988
|
+
warn(`[lsp] unexpected error acquiring install lock for ${lockKey}: ${err}`);
|
|
21989
|
+
return false;
|
|
21990
|
+
}
|
|
21991
|
+
};
|
|
21992
|
+
if (tryClaim())
|
|
21993
|
+
return true;
|
|
21994
|
+
let owningPid = null;
|
|
21995
|
+
let lockMtimeMs = 0;
|
|
21996
|
+
try {
|
|
21997
|
+
const raw = readFileSync2(lock, "utf8");
|
|
21998
|
+
const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
21999
|
+
const parsed = Number.parseInt(firstLine, 10);
|
|
22000
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
22001
|
+
owningPid = parsed;
|
|
22002
|
+
lockMtimeMs = statSync(lock).mtimeMs;
|
|
22003
|
+
} catch {
|
|
22004
|
+
return tryClaim();
|
|
22005
|
+
}
|
|
22006
|
+
const age = Date.now() - lockMtimeMs;
|
|
22007
|
+
const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS;
|
|
22008
|
+
const skipLiveness = process.platform === "win32";
|
|
22009
|
+
const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive(owningPid);
|
|
22010
|
+
if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
|
|
22011
|
+
return false;
|
|
22012
|
+
}
|
|
22013
|
+
log(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
|
|
22014
|
+
try {
|
|
22015
|
+
unlinkSync2(lock);
|
|
22016
|
+
} catch {}
|
|
22017
|
+
return tryClaim();
|
|
22018
|
+
}
|
|
22019
|
+
function isProcessAlive(pid) {
|
|
22020
|
+
try {
|
|
22021
|
+
process.kill(pid, 0);
|
|
22022
|
+
return true;
|
|
22023
|
+
} catch (err) {
|
|
22024
|
+
const code = err.code;
|
|
22025
|
+
if (code === "ESRCH")
|
|
22026
|
+
return false;
|
|
22027
|
+
return true;
|
|
22028
|
+
}
|
|
22029
|
+
}
|
|
22030
|
+
function releaseInstallLock(lockKey) {
|
|
22031
|
+
const lock = lockPath(lockKey);
|
|
22032
|
+
try {
|
|
22033
|
+
if (existsSync3(lock)) {
|
|
22034
|
+
unlinkSync2(lock);
|
|
22035
|
+
}
|
|
22036
|
+
} catch (err) {
|
|
22037
|
+
warn(`[lsp] failed to release install lock for ${lockKey}: ${err}`);
|
|
22038
|
+
}
|
|
22039
|
+
}
|
|
22040
|
+
async function withInstallLock(lockKey, task) {
|
|
22041
|
+
if (!acquireInstallLock(lockKey))
|
|
22042
|
+
return null;
|
|
22043
|
+
try {
|
|
22044
|
+
return await task();
|
|
22045
|
+
} finally {
|
|
22046
|
+
releaseInstallLock(lockKey);
|
|
22047
|
+
}
|
|
22048
|
+
}
|
|
22049
|
+
var VERSION_CHECK_FILE = ".aft-version-check";
|
|
22050
|
+
function readVersionCheck(npmPackage) {
|
|
22051
|
+
const file2 = join4(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
|
|
22052
|
+
try {
|
|
22053
|
+
const raw = readFileSync2(file2, "utf8");
|
|
22054
|
+
const parsed = JSON.parse(raw);
|
|
22055
|
+
if (typeof parsed.last_checked === "string") {
|
|
22056
|
+
return {
|
|
22057
|
+
last_checked: parsed.last_checked,
|
|
22058
|
+
latest_eligible: typeof parsed.latest_eligible === "string" ? parsed.latest_eligible : null
|
|
22059
|
+
};
|
|
22060
|
+
}
|
|
22061
|
+
return null;
|
|
22062
|
+
} catch {
|
|
22063
|
+
return null;
|
|
22064
|
+
}
|
|
22065
|
+
}
|
|
22066
|
+
function writeVersionCheck(npmPackage, latest) {
|
|
22067
|
+
mkdirSync2(lspPackageDir(npmPackage), { recursive: true });
|
|
22068
|
+
const file2 = join4(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
|
|
22069
|
+
const record2 = {
|
|
22070
|
+
last_checked: new Date().toISOString(),
|
|
22071
|
+
latest_eligible: latest
|
|
22072
|
+
};
|
|
22073
|
+
writeFileSync(file2, JSON.stringify(record2, null, 2));
|
|
22074
|
+
}
|
|
22075
|
+
function shouldRecheckVersion(record2, weeklyCheckIntervalMs = 7 * 24 * 60 * 60 * 1000) {
|
|
22076
|
+
if (!record2)
|
|
22077
|
+
return true;
|
|
22078
|
+
const age = Date.now() - new Date(record2.last_checked).getTime();
|
|
22079
|
+
if (Number.isNaN(age) || age < 0)
|
|
22080
|
+
return true;
|
|
22081
|
+
return age >= weeklyCheckIntervalMs;
|
|
22082
|
+
}
|
|
22083
|
+
|
|
22084
|
+
// src/lsp-github-probe.ts
|
|
22085
|
+
function pickEligibleRelease(releases, graceDays, now = Date.now()) {
|
|
22086
|
+
const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
|
|
22087
|
+
const candidates = releases.filter((r) => !r.draft && !r.prerelease && typeof r.published_at === "string").map((r) => {
|
|
22088
|
+
const ts = Date.parse(r.published_at);
|
|
22089
|
+
return { release: r, ts };
|
|
22090
|
+
}).filter((c) => !Number.isNaN(c.ts)).sort((a, b) => b.ts - a.ts);
|
|
22091
|
+
const eligible = candidates.filter((c) => c.ts <= cutoff);
|
|
22092
|
+
const blockedByGrace = candidates.length > 0 && eligible.length === 0;
|
|
22093
|
+
const chosen = eligible[0]?.release;
|
|
22094
|
+
if (!chosen) {
|
|
22095
|
+
return { tag: null, assets: [], blockedByGrace };
|
|
22096
|
+
}
|
|
22097
|
+
return {
|
|
22098
|
+
tag: chosen.tag_name,
|
|
22099
|
+
assets: (chosen.assets ?? []).map((a) => ({
|
|
22100
|
+
name: a.name,
|
|
22101
|
+
url: a.browser_download_url,
|
|
22102
|
+
size: a.size
|
|
22103
|
+
})),
|
|
22104
|
+
blockedByGrace: false
|
|
22105
|
+
};
|
|
22106
|
+
}
|
|
22107
|
+
async function probeGithubReleases(githubRepo, graceDays, fetchImpl = fetch) {
|
|
22108
|
+
const url2 = `https://api.github.com/repos/${githubRepo}/releases?per_page=30`;
|
|
22109
|
+
try {
|
|
22110
|
+
const headers = { accept: "application/vnd.github+json" };
|
|
22111
|
+
if (process.env.GITHUB_TOKEN) {
|
|
22112
|
+
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
22113
|
+
}
|
|
22114
|
+
const res = await fetchImpl(url2, {
|
|
22115
|
+
headers,
|
|
22116
|
+
signal: AbortSignal.timeout(1e4)
|
|
22117
|
+
});
|
|
22118
|
+
if (!res.ok) {
|
|
22119
|
+
warn(`[lsp] github releases probe failed for ${githubRepo}: HTTP ${res.status}`);
|
|
22120
|
+
return null;
|
|
22121
|
+
}
|
|
22122
|
+
const json2 = await res.json();
|
|
22123
|
+
if (!Array.isArray(json2)) {
|
|
22124
|
+
warn(`[lsp] unexpected response shape from github releases for ${githubRepo}`);
|
|
22125
|
+
return null;
|
|
22126
|
+
}
|
|
22127
|
+
return pickEligibleRelease(json2, graceDays);
|
|
22128
|
+
} catch (err) {
|
|
22129
|
+
warn(`[lsp] github releases probe failed for ${githubRepo}: ${err}`);
|
|
22130
|
+
return null;
|
|
22131
|
+
}
|
|
22132
|
+
}
|
|
22133
|
+
var SAFE_VERSION_RE = /^[A-Za-z0-9._+-]+$/;
|
|
22134
|
+
function assertSafeVersion(version2) {
|
|
22135
|
+
if (!SAFE_VERSION_RE.test(version2)) {
|
|
22136
|
+
throw new Error(`unsafe version/tag string ${JSON.stringify(version2)}: must match ${SAFE_VERSION_RE.source}`);
|
|
22137
|
+
}
|
|
22138
|
+
}
|
|
22139
|
+
function stripTagV(tag) {
|
|
22140
|
+
assertSafeVersion(tag);
|
|
22141
|
+
return tag.startsWith("v") ? tag.slice(1) : tag;
|
|
22142
|
+
}
|
|
22143
|
+
|
|
22144
|
+
// src/lsp-npm-table.ts
|
|
22145
|
+
var NPM_LSP_TABLE = [
|
|
22146
|
+
{
|
|
22147
|
+
id: "typescript",
|
|
22148
|
+
npm: "typescript-language-server",
|
|
22149
|
+
binary: "typescript-language-server",
|
|
22150
|
+
extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
|
|
22151
|
+
rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
|
|
22152
|
+
},
|
|
22153
|
+
{
|
|
22154
|
+
id: "python",
|
|
22155
|
+
npm: "pyright",
|
|
22156
|
+
binary: "pyright-langserver",
|
|
22157
|
+
extensions: ["py", "pyi"],
|
|
22158
|
+
rootMarkers: ["pyproject.toml", "pyrightconfig.json", "requirements.txt"]
|
|
22159
|
+
},
|
|
22160
|
+
{
|
|
22161
|
+
id: "yaml",
|
|
22162
|
+
npm: "yaml-language-server",
|
|
22163
|
+
binary: "yaml-language-server",
|
|
22164
|
+
extensions: ["yaml", "yml"]
|
|
22165
|
+
},
|
|
22166
|
+
{
|
|
22167
|
+
id: "bash",
|
|
22168
|
+
npm: "bash-language-server",
|
|
22169
|
+
binary: "bash-language-server",
|
|
22170
|
+
extensions: ["sh", "bash", "zsh"]
|
|
22171
|
+
},
|
|
22172
|
+
{
|
|
22173
|
+
id: "dockerfile",
|
|
22174
|
+
npm: "dockerfile-language-server-nodejs",
|
|
22175
|
+
binary: "docker-langserver",
|
|
22176
|
+
extensions: ["dockerfile"],
|
|
22177
|
+
rootMarkers: ["Dockerfile", "dockerfile"]
|
|
22178
|
+
},
|
|
22179
|
+
{
|
|
22180
|
+
id: "vue",
|
|
22181
|
+
npm: "@vue/language-server",
|
|
22182
|
+
binary: "vue-language-server",
|
|
22183
|
+
extensions: ["vue"]
|
|
22184
|
+
},
|
|
22185
|
+
{
|
|
22186
|
+
id: "astro",
|
|
22187
|
+
npm: "@astrojs/language-server",
|
|
22188
|
+
binary: "astro-ls",
|
|
22189
|
+
extensions: ["astro"]
|
|
22190
|
+
},
|
|
22191
|
+
{
|
|
22192
|
+
id: "svelte",
|
|
22193
|
+
npm: "svelte-language-server",
|
|
22194
|
+
binary: "svelteserver",
|
|
22195
|
+
extensions: ["svelte"]
|
|
22196
|
+
},
|
|
22197
|
+
{
|
|
22198
|
+
id: "php-intelephense",
|
|
22199
|
+
npm: "intelephense",
|
|
22200
|
+
binary: "intelephense",
|
|
22201
|
+
extensions: ["php"]
|
|
22202
|
+
},
|
|
22203
|
+
{
|
|
22204
|
+
id: "biome",
|
|
22205
|
+
npm: "@biomejs/biome",
|
|
22206
|
+
binary: "biome",
|
|
22207
|
+
extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts", "json", "jsonc"],
|
|
22208
|
+
rootMarkers: ["biome.json", "biome.jsonc"]
|
|
22209
|
+
}
|
|
22210
|
+
];
|
|
22211
|
+
|
|
22212
|
+
// src/lsp-project-relevance.ts
|
|
22213
|
+
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
22214
|
+
import { join as join5 } from "path";
|
|
22215
|
+
var MAX_WALK_DIRS = 200;
|
|
22216
|
+
var MAX_WALK_DEPTH = 4;
|
|
22217
|
+
var NOISE_DIRS = new Set([
|
|
22218
|
+
".git",
|
|
22219
|
+
".next",
|
|
22220
|
+
".venv",
|
|
22221
|
+
"__pycache__",
|
|
22222
|
+
"build",
|
|
22223
|
+
"dist",
|
|
22224
|
+
"node_modules",
|
|
22225
|
+
"target"
|
|
22226
|
+
]);
|
|
22227
|
+
function hasRootMarker(projectRoot, rootMarkers) {
|
|
22228
|
+
if (!rootMarkers)
|
|
22229
|
+
return false;
|
|
22230
|
+
for (const marker of rootMarkers) {
|
|
22231
|
+
if (existsSync4(join5(projectRoot, marker)))
|
|
22232
|
+
return true;
|
|
22233
|
+
}
|
|
22234
|
+
return false;
|
|
22235
|
+
}
|
|
22236
|
+
function relevantExtensionsInProject(projectRoot, extToServer) {
|
|
22237
|
+
const wanted = new Set(Object.keys(extToServer).map((ext) => ext.toLowerCase()));
|
|
22238
|
+
const found = new Set;
|
|
22239
|
+
if (wanted.size === 0)
|
|
22240
|
+
return found;
|
|
22241
|
+
const queue = [{ dir: projectRoot, depth: 0 }];
|
|
22242
|
+
let visitedDirs = 0;
|
|
22243
|
+
while (queue.length > 0 && visitedDirs < MAX_WALK_DIRS) {
|
|
22244
|
+
const current = queue.shift();
|
|
22245
|
+
if (!current)
|
|
22246
|
+
break;
|
|
22247
|
+
visitedDirs += 1;
|
|
22248
|
+
let entries;
|
|
22249
|
+
try {
|
|
22250
|
+
entries = readdirSync(current.dir, { withFileTypes: true });
|
|
22251
|
+
} catch {
|
|
22252
|
+
continue;
|
|
22253
|
+
}
|
|
22254
|
+
for (const entry of entries) {
|
|
22255
|
+
if (entry.isDirectory()) {
|
|
22256
|
+
if (current.depth < MAX_WALK_DEPTH && !NOISE_DIRS.has(entry.name.toLowerCase())) {
|
|
22257
|
+
queue.push({ dir: join5(current.dir, entry.name), depth: current.depth + 1 });
|
|
22258
|
+
}
|
|
22259
|
+
continue;
|
|
22260
|
+
}
|
|
22261
|
+
if (!entry.isFile())
|
|
22262
|
+
continue;
|
|
22263
|
+
const ext = extensionOf(entry.name);
|
|
22264
|
+
if (ext && wanted.has(ext))
|
|
22265
|
+
found.add(ext);
|
|
22266
|
+
}
|
|
22267
|
+
}
|
|
22268
|
+
return found;
|
|
22269
|
+
}
|
|
22270
|
+
function extensionOf(fileName) {
|
|
22271
|
+
const dot = fileName.lastIndexOf(".");
|
|
22272
|
+
if (dot < 0 || dot === fileName.length - 1)
|
|
22273
|
+
return null;
|
|
22274
|
+
return fileName.slice(dot + 1).toLowerCase();
|
|
22275
|
+
}
|
|
22276
|
+
|
|
22277
|
+
// src/lsp-registry-probe.ts
|
|
22278
|
+
var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
|
|
22279
|
+
function pickEligibleVersion(response, graceDays, now = Date.now()) {
|
|
22280
|
+
const times = response.time || {};
|
|
22281
|
+
const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
|
|
22282
|
+
const candidates = [];
|
|
22283
|
+
for (const [version2, publishedAt] of Object.entries(times)) {
|
|
22284
|
+
if (version2 === "created" || version2 === "modified")
|
|
22285
|
+
continue;
|
|
22286
|
+
if (version2.includes("-"))
|
|
22287
|
+
continue;
|
|
22288
|
+
if (typeof publishedAt !== "string")
|
|
22289
|
+
continue;
|
|
22290
|
+
const ts = Date.parse(publishedAt);
|
|
22291
|
+
if (Number.isNaN(ts))
|
|
22292
|
+
continue;
|
|
22293
|
+
candidates.push({ version: version2, publishedAt, ts });
|
|
22294
|
+
}
|
|
22295
|
+
candidates.sort((a, b) => b.ts - a.ts);
|
|
22296
|
+
const eligible = candidates.filter((c) => c.ts <= cutoff);
|
|
22297
|
+
const blockedByGrace = candidates.length > 0 && eligible.length === 0;
|
|
22298
|
+
return {
|
|
22299
|
+
version: eligible[0]?.version ?? null,
|
|
22300
|
+
blockedByGrace,
|
|
22301
|
+
eligible: eligible.map(({ version: version2, publishedAt }) => ({ version: version2, publishedAt }))
|
|
22302
|
+
};
|
|
22303
|
+
}
|
|
22304
|
+
async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
|
|
22305
|
+
const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
|
|
22306
|
+
const url2 = `${NPM_REGISTRY_BASE}/${encoded}`;
|
|
22307
|
+
try {
|
|
22308
|
+
const res = await fetchImpl(url2, {
|
|
22309
|
+
headers: { accept: "application/json" },
|
|
22310
|
+
signal: AbortSignal.timeout(1e4)
|
|
22311
|
+
});
|
|
22312
|
+
if (!res.ok) {
|
|
22313
|
+
warn(`[lsp] registry probe failed for ${npmPackage}: HTTP ${res.status}`);
|
|
22314
|
+
return null;
|
|
22315
|
+
}
|
|
22316
|
+
const json2 = await res.json();
|
|
22317
|
+
return pickEligibleVersion(json2, graceDays);
|
|
22318
|
+
} catch (err) {
|
|
22319
|
+
warn(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
|
|
22320
|
+
return null;
|
|
22321
|
+
}
|
|
22322
|
+
}
|
|
22323
|
+
|
|
22324
|
+
// src/lsp-auto-install.ts
|
|
22325
|
+
function isProjectRelevant(spec, projectRoot, projectExtensions) {
|
|
22326
|
+
if (hasRootMarker(projectRoot, spec.rootMarkers))
|
|
22327
|
+
return true;
|
|
22328
|
+
const extensions = projectExtensions();
|
|
22329
|
+
return spec.extensions.some((ext) => extensions.has(ext.toLowerCase()));
|
|
22330
|
+
}
|
|
22331
|
+
var npmExtToServerIds = buildExtensionMap(NPM_LSP_TABLE);
|
|
22332
|
+
function buildExtensionMap(specs) {
|
|
22333
|
+
const byExt = {};
|
|
22334
|
+
for (const spec of specs) {
|
|
22335
|
+
for (const ext of spec.extensions) {
|
|
22336
|
+
const key = ext.toLowerCase();
|
|
22337
|
+
byExt[key] ??= [];
|
|
22338
|
+
byExt[key].push(spec.id);
|
|
22339
|
+
}
|
|
22340
|
+
}
|
|
22341
|
+
return byExt;
|
|
22342
|
+
}
|
|
22343
|
+
var inFlightAutoInstalls = new Set;
|
|
22344
|
+
function trackInFlightAutoInstall(controller, promise2) {
|
|
22345
|
+
const entry = { controller, promise: promise2 };
|
|
22346
|
+
inFlightAutoInstalls.add(entry);
|
|
22347
|
+
promise2.then(() => inFlightAutoInstalls.delete(entry), () => inFlightAutoInstalls.delete(entry));
|
|
22348
|
+
return promise2;
|
|
22349
|
+
}
|
|
22350
|
+
async function abortInFlightAutoInstalls() {
|
|
22351
|
+
const installs = Array.from(inFlightAutoInstalls);
|
|
22352
|
+
for (const install of installs) {
|
|
22353
|
+
install.controller.abort();
|
|
22354
|
+
}
|
|
22355
|
+
await Promise.allSettled(installs.map((install) => install.promise));
|
|
22356
|
+
}
|
|
22357
|
+
async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
|
|
22358
|
+
const pinned = config2.versions[spec.npm];
|
|
22359
|
+
if (pinned) {
|
|
22360
|
+
assertSafeVersion(pinned);
|
|
22361
|
+
return { version: pinned, pinned: true, probe: null };
|
|
22362
|
+
}
|
|
22363
|
+
const cached2 = readVersionCheck(spec.npm);
|
|
22364
|
+
const weeklyMs = config2.graceDays * 24 * 60 * 60 * 1000;
|
|
22365
|
+
if (!shouldRecheckVersion(cached2, weeklyMs) && cached2?.latest_eligible) {
|
|
22366
|
+
return { version: cached2.latest_eligible, pinned: false, probe: null };
|
|
22367
|
+
}
|
|
22368
|
+
const probe = await probeRegistry(spec.npm, config2.graceDays, fetchImpl);
|
|
22369
|
+
if (!probe) {
|
|
22370
|
+
return {
|
|
22371
|
+
version: cached2?.latest_eligible ?? null,
|
|
22372
|
+
pinned: false,
|
|
22373
|
+
probe: null
|
|
22374
|
+
};
|
|
22375
|
+
}
|
|
22376
|
+
writeVersionCheck(spec.npm, probe.version);
|
|
22377
|
+
return { version: probe.version, pinned: false, probe };
|
|
22378
|
+
}
|
|
22379
|
+
function runInstall(spec, version2, cwd, signal) {
|
|
22380
|
+
return new Promise((resolve) => {
|
|
22381
|
+
const target = `${spec.npm}@${version2}`;
|
|
22382
|
+
log(`[lsp] installing ${target} to ${cwd}`);
|
|
22383
|
+
if (signal?.aborted) {
|
|
22384
|
+
warn(`[lsp] install ${target} aborted before spawn`);
|
|
22385
|
+
resolve(false);
|
|
22386
|
+
return;
|
|
22387
|
+
}
|
|
22388
|
+
const child = spawn("bun", ["add", target, "--cwd", cwd, "--ignore-scripts", "--silent"], {
|
|
22389
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
22390
|
+
});
|
|
22391
|
+
child.unref();
|
|
22392
|
+
let stderrBuf = "";
|
|
22393
|
+
let settled = false;
|
|
22394
|
+
let killTimer = null;
|
|
22395
|
+
const cleanup = () => {
|
|
22396
|
+
signal?.removeEventListener("abort", onAbort);
|
|
22397
|
+
if (killTimer)
|
|
22398
|
+
clearTimeout(killTimer);
|
|
22399
|
+
};
|
|
22400
|
+
const finish = (ok) => {
|
|
22401
|
+
if (settled)
|
|
22402
|
+
return;
|
|
22403
|
+
settled = true;
|
|
22404
|
+
cleanup();
|
|
22405
|
+
resolve(ok);
|
|
22406
|
+
};
|
|
22407
|
+
const onAbort = () => {
|
|
22408
|
+
warn(`[lsp] install ${target} aborted during shutdown`);
|
|
22409
|
+
child.kill("SIGTERM");
|
|
22410
|
+
killTimer = setTimeout(() => {
|
|
22411
|
+
if (!settled)
|
|
22412
|
+
child.kill("SIGKILL");
|
|
22413
|
+
}, 5000);
|
|
22414
|
+
killTimer.unref?.();
|
|
22415
|
+
};
|
|
22416
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
22417
|
+
if (signal?.aborted)
|
|
22418
|
+
onAbort();
|
|
22419
|
+
child.stdout?.on("data", () => {});
|
|
22420
|
+
child.stderr?.on("data", (chunk) => {
|
|
22421
|
+
const text = String(chunk);
|
|
22422
|
+
stderrBuf += text;
|
|
22423
|
+
if (stderrBuf.length > 4096) {
|
|
22424
|
+
stderrBuf = stderrBuf.slice(stderrBuf.length - 4096);
|
|
22425
|
+
}
|
|
22426
|
+
});
|
|
22427
|
+
child.on("error", (err) => {
|
|
22428
|
+
error48(`[lsp] install ${target} failed to spawn: ${err}`);
|
|
22429
|
+
finish(false);
|
|
22430
|
+
});
|
|
22431
|
+
child.on("exit", (code) => {
|
|
22432
|
+
if (code === 0) {
|
|
22433
|
+
log(`[lsp] installed ${target}`);
|
|
22434
|
+
finish(true);
|
|
22435
|
+
} else {
|
|
22436
|
+
error48(`[lsp] install ${target} exited with code ${code}; last stderr:
|
|
22437
|
+
${stderrBuf.trim()}`);
|
|
22438
|
+
finish(false);
|
|
22439
|
+
}
|
|
22440
|
+
});
|
|
22441
|
+
});
|
|
22442
|
+
}
|
|
22443
|
+
async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
|
|
22444
|
+
const outcome = await withInstallLock(spec.npm, async () => {
|
|
22445
|
+
const { version: version2, probe } = await resolveTargetVersion(spec, config2, fetchImpl);
|
|
22446
|
+
if (!version2) {
|
|
22447
|
+
const installed = isInstalled(spec.npm, spec.binary);
|
|
22448
|
+
if (installed) {
|
|
22449
|
+
warn(`[lsp] no eligible version of ${spec.npm} (grace=${config2.graceDays}d); keeping existing install`);
|
|
22450
|
+
return { started: false, reason: "kept existing install" };
|
|
22451
|
+
}
|
|
22452
|
+
const blocked = probe?.blockedByGrace ? `all versions are within ${config2.graceDays}-day grace window` : "registry probe failed";
|
|
22453
|
+
warn(`[lsp] skipping ${spec.npm}: ${blocked}`);
|
|
22454
|
+
return { started: false, reason: blocked };
|
|
22455
|
+
}
|
|
22456
|
+
if (isInstalled(spec.npm, spec.binary)) {
|
|
22457
|
+
const installedMeta = readInstalledMeta(spec.npm);
|
|
22458
|
+
if (installedMeta && installedMeta.version === version2) {
|
|
22459
|
+
if (installedMeta.sha256) {
|
|
22460
|
+
const currentHash = await hashInstalledBinary(spec).catch((err) => {
|
|
22461
|
+
warn(`[lsp] could not hash existing ${spec.npm} binary for TOFU check: ${err}`);
|
|
22462
|
+
return null;
|
|
22463
|
+
});
|
|
22464
|
+
if (currentHash && currentHash !== installedMeta.sha256) {
|
|
22465
|
+
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.`);
|
|
22466
|
+
return {
|
|
22467
|
+
started: false,
|
|
22468
|
+
reason: `TOFU sha256 mismatch on ${spec.npm}@${version2} \u2014 see plugin log`
|
|
22469
|
+
};
|
|
22470
|
+
}
|
|
22471
|
+
}
|
|
22472
|
+
return { started: false, reason: "already installed" };
|
|
22473
|
+
}
|
|
22474
|
+
if (installedMeta) {
|
|
22475
|
+
log(`[lsp] reinstalling ${spec.npm}: cached ${installedMeta.version} \u2260 target ${version2}`);
|
|
22476
|
+
} else {
|
|
22477
|
+
log(`[lsp] reinstalling ${spec.npm}@${version2}: no installed-version metadata recorded`);
|
|
22478
|
+
}
|
|
22479
|
+
}
|
|
22480
|
+
const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), signal).catch((err) => {
|
|
22481
|
+
error48(`[lsp] background install ${spec.npm} crashed: ${err}`);
|
|
22482
|
+
return false;
|
|
22483
|
+
});
|
|
22484
|
+
if (!ok) {
|
|
22485
|
+
return { started: true, reason: "install failed (see plugin log)" };
|
|
22486
|
+
}
|
|
22487
|
+
const installedHash = await hashInstalledBinary(spec).catch((err) => {
|
|
22488
|
+
warn(`[lsp] could not hash newly-installed ${spec.npm} binary: ${err}`);
|
|
22489
|
+
return null;
|
|
22490
|
+
});
|
|
22491
|
+
if (installedHash) {
|
|
22492
|
+
log(`[lsp] ${spec.npm}@${version2} installed sha256=${installedHash}`);
|
|
22493
|
+
}
|
|
22494
|
+
writeInstalledMeta(spec.npm, version2, installedHash ?? undefined);
|
|
22495
|
+
return { started: true };
|
|
22496
|
+
});
|
|
22497
|
+
if (outcome === null) {
|
|
22498
|
+
return { started: false, reason: "another install in progress" };
|
|
22499
|
+
}
|
|
22500
|
+
return outcome;
|
|
22501
|
+
}
|
|
22502
|
+
function cachedPackageDir(npmPackage) {
|
|
22503
|
+
return lspBinDir(npmPackage).replace(/[\\/]node_modules[\\/]\.bin[\\/]?$/, "");
|
|
22504
|
+
}
|
|
22505
|
+
function hashInstalledBinary(spec) {
|
|
22506
|
+
return new Promise((resolve, reject) => {
|
|
22507
|
+
const candidates = process.platform === "win32" ? [
|
|
22508
|
+
lspBinaryPath(spec.npm, spec.binary),
|
|
22509
|
+
lspBinaryPath(spec.npm, `${spec.binary}.cmd`),
|
|
22510
|
+
lspBinaryPath(spec.npm, `${spec.binary}.exe`),
|
|
22511
|
+
lspBinaryPath(spec.npm, `${spec.binary}.bat`)
|
|
22512
|
+
] : [lspBinaryPath(spec.npm, spec.binary)];
|
|
22513
|
+
let pathToHash = null;
|
|
22514
|
+
for (const p of candidates) {
|
|
22515
|
+
try {
|
|
22516
|
+
if (statSync2(p).isFile()) {
|
|
22517
|
+
pathToHash = p;
|
|
22518
|
+
break;
|
|
22519
|
+
}
|
|
22520
|
+
} catch {}
|
|
22521
|
+
}
|
|
22522
|
+
if (!pathToHash) {
|
|
22523
|
+
reject(new Error(`installed binary not found at any of: ${candidates.join(", ")}`));
|
|
22524
|
+
return;
|
|
22525
|
+
}
|
|
22526
|
+
const hash2 = createHash("sha256");
|
|
22527
|
+
const stream = createReadStream(pathToHash);
|
|
22528
|
+
stream.on("error", reject);
|
|
22529
|
+
stream.on("data", (chunk) => hash2.update(chunk));
|
|
22530
|
+
stream.on("end", () => resolve(hash2.digest("hex")));
|
|
22531
|
+
});
|
|
22532
|
+
}
|
|
22533
|
+
function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
|
|
22534
|
+
const cachedBinDirs = [];
|
|
22535
|
+
const skipped = [];
|
|
22536
|
+
const installPromises = [];
|
|
22537
|
+
let installsStarted = 0;
|
|
22538
|
+
let projectExtensions = null;
|
|
22539
|
+
const getProjectExtensions = () => {
|
|
22540
|
+
projectExtensions ??= relevantExtensionsInProject(projectRoot, npmExtToServerIds);
|
|
22541
|
+
return projectExtensions;
|
|
22542
|
+
};
|
|
22543
|
+
for (const spec of NPM_LSP_TABLE) {
|
|
22544
|
+
if (isInstalled(spec.npm, spec.binary)) {
|
|
22545
|
+
cachedBinDirs.push(lspBinDir(spec.npm));
|
|
22546
|
+
}
|
|
22547
|
+
if (config2.disabled.has(spec.id)) {
|
|
22548
|
+
skipped.push({ id: spec.id, reason: "disabled by config" });
|
|
22549
|
+
continue;
|
|
22550
|
+
}
|
|
22551
|
+
if (!config2.autoInstall) {
|
|
22552
|
+
skipped.push({ id: spec.id, reason: "auto_install: false" });
|
|
22553
|
+
continue;
|
|
22554
|
+
}
|
|
22555
|
+
if (!isProjectRelevant(spec, projectRoot, getProjectExtensions)) {
|
|
22556
|
+
skipped.push({ id: spec.id, reason: "not relevant to project" });
|
|
22557
|
+
continue;
|
|
22558
|
+
}
|
|
22559
|
+
installsStarted += 1;
|
|
22560
|
+
const controller = new AbortController;
|
|
22561
|
+
const promise2 = ensureServerInstalled(spec, config2, fetchImpl, controller.signal).then((outcome) => {
|
|
22562
|
+
if (!outcome.started)
|
|
22563
|
+
installsStarted -= 1;
|
|
22564
|
+
if (outcome.reason && outcome.reason !== "already installed") {
|
|
22565
|
+
skipped.push({ id: spec.id, reason: outcome.reason });
|
|
22566
|
+
}
|
|
22567
|
+
}, (err) => {
|
|
22568
|
+
installsStarted -= 1;
|
|
22569
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
22570
|
+
skipped.push({ id: spec.id, reason: `install error: ${reason}` });
|
|
22571
|
+
error48(`[lsp] background install ${spec.npm} promise rejected: ${reason}`);
|
|
22572
|
+
});
|
|
22573
|
+
installPromises.push(trackInFlightAutoInstall(controller, promise2));
|
|
22574
|
+
}
|
|
22575
|
+
return {
|
|
22576
|
+
cachedBinDirs,
|
|
22577
|
+
get installsStarted() {
|
|
22578
|
+
return installsStarted;
|
|
22579
|
+
},
|
|
22580
|
+
skipped,
|
|
22581
|
+
installsComplete: Promise.all(installPromises).then(() => {})
|
|
22582
|
+
};
|
|
22583
|
+
}
|
|
22584
|
+
|
|
22585
|
+
// src/lsp-github-install.ts
|
|
22586
|
+
import { execFileSync } from "child_process";
|
|
22587
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
22588
|
+
import {
|
|
22589
|
+
copyFileSync,
|
|
22590
|
+
createReadStream as createReadStream2,
|
|
22591
|
+
createWriteStream,
|
|
22592
|
+
existsSync as existsSync5,
|
|
22593
|
+
lstatSync,
|
|
22594
|
+
mkdirSync as mkdirSync3,
|
|
22595
|
+
readdirSync as readdirSync2,
|
|
22596
|
+
readlinkSync,
|
|
22597
|
+
realpathSync,
|
|
22598
|
+
renameSync,
|
|
22599
|
+
rmSync,
|
|
22600
|
+
statSync as statSync3,
|
|
22601
|
+
unlinkSync as unlinkSync3
|
|
22602
|
+
} from "fs";
|
|
22603
|
+
import { dirname, join as join6, relative, resolve } from "path";
|
|
22604
|
+
import { Readable } from "stream";
|
|
22605
|
+
import { pipeline } from "stream/promises";
|
|
22606
|
+
|
|
22607
|
+
// src/lsp-github-table.ts
|
|
22608
|
+
function exe(platform, name) {
|
|
22609
|
+
return platform === "win32" ? `${name}.exe` : name;
|
|
22610
|
+
}
|
|
22611
|
+
var CLANGD = {
|
|
22612
|
+
id: "clangd",
|
|
22613
|
+
githubRepo: "clangd/clangd",
|
|
22614
|
+
binary: "clangd",
|
|
22615
|
+
resolveAsset: (platform, _arch, version2) => {
|
|
22616
|
+
const platformName = platform === "darwin" ? "mac" : platform === "linux" ? "linux" : "windows";
|
|
22617
|
+
return { name: `clangd-${platformName}-${version2}.zip`, archive: "zip" };
|
|
22618
|
+
},
|
|
22619
|
+
binaryPathInArchive: (platform, _arch, version2) => `clangd_${version2}/bin/${exe(platform, "clangd")}`
|
|
22620
|
+
};
|
|
22621
|
+
var LUA_LS = {
|
|
22622
|
+
id: "lua-ls",
|
|
22623
|
+
githubRepo: "LuaLS/lua-language-server",
|
|
22624
|
+
binary: "lua-language-server",
|
|
22625
|
+
resolveAsset: (platform, arch, version2) => {
|
|
22626
|
+
const ext = platform === "win32" ? "zip" : "tar.gz";
|
|
22627
|
+
const platformName = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : "win32";
|
|
22628
|
+
const archName = arch === "arm64" ? "arm64" : "x64";
|
|
22629
|
+
return {
|
|
22630
|
+
name: `lua-language-server-${version2}-${platformName}-${archName}.${ext}`,
|
|
22631
|
+
archive: ext
|
|
22632
|
+
};
|
|
22633
|
+
},
|
|
22634
|
+
binaryPathInArchive: (platform, _arch, _version) => `bin/${exe(platform, "lua-language-server")}`
|
|
22635
|
+
};
|
|
22636
|
+
var ZLS = {
|
|
22637
|
+
id: "zls",
|
|
22638
|
+
githubRepo: "zigtools/zls",
|
|
22639
|
+
binary: "zls",
|
|
22640
|
+
resolveAsset: (platform, arch, _version) => {
|
|
22641
|
+
const ext = platform === "win32" ? "zip" : "tar.xz";
|
|
22642
|
+
const archName = arch === "arm64" ? "aarch64" : "x86_64";
|
|
22643
|
+
const platformName = platform === "darwin" ? "macos" : platform === "linux" ? "linux" : "windows";
|
|
22644
|
+
return { name: `zls-${archName}-${platformName}.${ext}`, archive: ext };
|
|
22645
|
+
},
|
|
22646
|
+
binaryPathInArchive: (platform, _arch, _version) => exe(platform, "zls")
|
|
22647
|
+
};
|
|
22648
|
+
var TINYMIST = {
|
|
22649
|
+
id: "tinymist",
|
|
22650
|
+
githubRepo: "Myriad-Dreamin/tinymist",
|
|
22651
|
+
binary: "tinymist",
|
|
22652
|
+
resolveAsset: (platform, arch, _version) => {
|
|
22653
|
+
const archName = arch === "arm64" ? "aarch64" : "x86_64";
|
|
22654
|
+
const triple = platform === "darwin" ? "apple-darwin" : platform === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
|
|
22655
|
+
const ext = platform === "win32" ? "zip" : "tar.gz";
|
|
22656
|
+
return { name: `tinymist-${archName}-${triple}.${ext}`, archive: ext };
|
|
22657
|
+
},
|
|
22658
|
+
binaryPathInArchive: (platform, _arch, _version) => exe(platform, "tinymist")
|
|
22659
|
+
};
|
|
22660
|
+
var TEXLAB = {
|
|
22661
|
+
id: "texlab",
|
|
22662
|
+
githubRepo: "latex-lsp/texlab",
|
|
22663
|
+
binary: "texlab",
|
|
22664
|
+
resolveAsset: (platform, arch, _version) => {
|
|
22665
|
+
const archName = arch === "arm64" ? "aarch64" : "x86_64";
|
|
22666
|
+
const platformName = platform === "darwin" ? "macos" : platform === "linux" ? "linux" : "windows";
|
|
22667
|
+
const ext = platform === "win32" ? "zip" : "tar.gz";
|
|
22668
|
+
return { name: `texlab-${archName}-${platformName}.${ext}`, archive: ext };
|
|
22669
|
+
},
|
|
22670
|
+
binaryPathInArchive: (platform, _arch, _version) => exe(platform, "texlab")
|
|
22671
|
+
};
|
|
22672
|
+
var GITHUB_LSP_TABLE = [
|
|
22673
|
+
CLANGD,
|
|
22674
|
+
LUA_LS,
|
|
22675
|
+
ZLS,
|
|
22676
|
+
TINYMIST,
|
|
22677
|
+
TEXLAB
|
|
22678
|
+
];
|
|
22679
|
+
function detectHostPlatform() {
|
|
22680
|
+
const platform = process.platform;
|
|
22681
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32")
|
|
22682
|
+
return null;
|
|
22683
|
+
const arch = process.arch;
|
|
22684
|
+
if (arch === "x64")
|
|
22685
|
+
return { platform, arch: "x64" };
|
|
22686
|
+
if (arch === "arm64")
|
|
22687
|
+
return { platform, arch: "arm64" };
|
|
22688
|
+
return null;
|
|
22689
|
+
}
|
|
22690
|
+
|
|
22691
|
+
// src/lsp-github-install.ts
|
|
22692
|
+
function ghCacheRoot() {
|
|
22693
|
+
return join6(aftCacheBase(), "lsp-binaries");
|
|
22694
|
+
}
|
|
22695
|
+
function ghPackageDir(spec) {
|
|
22696
|
+
return join6(ghCacheRoot(), spec.id);
|
|
22697
|
+
}
|
|
22698
|
+
function ghBinDir(spec) {
|
|
22699
|
+
return join6(ghPackageDir(spec), "bin");
|
|
22700
|
+
}
|
|
22701
|
+
function ghExtractDir(spec) {
|
|
22702
|
+
return join6(ghPackageDir(spec), "extracted");
|
|
22703
|
+
}
|
|
22704
|
+
function ghBinaryPath(spec, platform) {
|
|
22705
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
22706
|
+
return join6(ghBinDir(spec), `${spec.binary}${ext}`);
|
|
22707
|
+
}
|
|
22708
|
+
function isGithubInstalled(spec, platform) {
|
|
22709
|
+
for (const candidate of ghBinaryCandidates(spec, platform)) {
|
|
22710
|
+
try {
|
|
22711
|
+
if (statSync3(join6(ghBinDir(spec), candidate)).isFile())
|
|
22712
|
+
return true;
|
|
22713
|
+
} catch {}
|
|
22714
|
+
}
|
|
22715
|
+
return false;
|
|
22716
|
+
}
|
|
22717
|
+
function ghBinaryCandidates(spec, platform) {
|
|
22718
|
+
if (platform !== "win32")
|
|
22719
|
+
return [spec.binary];
|
|
22720
|
+
return [spec.binary, `${spec.binary}.cmd`, `${spec.binary}.exe`, `${spec.binary}.bat`];
|
|
22721
|
+
}
|
|
22722
|
+
var MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
|
|
22723
|
+
var MAX_EXTRACT_BYTES = 1024 * 1024 * 1024;
|
|
22724
|
+
function sha256OfFile(path2) {
|
|
22725
|
+
return new Promise((resolve2, reject) => {
|
|
22726
|
+
const hash2 = createHash2("sha256");
|
|
22727
|
+
const stream = createReadStream2(path2);
|
|
22728
|
+
stream.on("error", reject);
|
|
22729
|
+
stream.on("data", (chunk) => hash2.update(chunk));
|
|
22730
|
+
stream.on("end", () => resolve2(hash2.digest("hex")));
|
|
22731
|
+
});
|
|
22732
|
+
}
|
|
22733
|
+
async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
|
|
22734
|
+
const candidates = [];
|
|
22735
|
+
candidates.push(tag);
|
|
22736
|
+
if (!tag.startsWith("v")) {
|
|
22737
|
+
candidates.push(`v${tag}`);
|
|
22738
|
+
} else {
|
|
22739
|
+
candidates.push(tag.slice(1));
|
|
22740
|
+
}
|
|
22741
|
+
const headers = {
|
|
22742
|
+
accept: "application/vnd.github+json",
|
|
22743
|
+
"user-agent": "aft-opencode",
|
|
22744
|
+
"x-github-api-version": "2022-11-28"
|
|
22745
|
+
};
|
|
22746
|
+
if (process.env.GITHUB_TOKEN) {
|
|
22747
|
+
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
22748
|
+
}
|
|
22749
|
+
for (const candidate of candidates) {
|
|
22750
|
+
const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
|
|
22751
|
+
const timeout = controlledTimeoutSignal(15000, signal);
|
|
22752
|
+
try {
|
|
22753
|
+
const res = await fetchImpl(url2, {
|
|
22754
|
+
headers,
|
|
22755
|
+
redirect: "follow",
|
|
22756
|
+
signal: timeout.signal
|
|
22757
|
+
});
|
|
22758
|
+
if (res.status === 404)
|
|
22759
|
+
continue;
|
|
22760
|
+
if (!res.ok) {
|
|
22761
|
+
warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: HTTP ${res.status}`);
|
|
22762
|
+
return null;
|
|
22763
|
+
}
|
|
22764
|
+
const json2 = await res.json();
|
|
22765
|
+
if (!json2.tag_name || !Array.isArray(json2.assets)) {
|
|
22766
|
+
warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: malformed response`);
|
|
22767
|
+
return null;
|
|
22768
|
+
}
|
|
22769
|
+
const assets = json2.assets.filter((a) => typeof a.name === "string" && typeof a.browser_download_url === "string").map((a) => ({
|
|
22770
|
+
name: a.name,
|
|
22771
|
+
url: a.browser_download_url,
|
|
22772
|
+
size: typeof a.size === "number" ? a.size : undefined
|
|
22773
|
+
}));
|
|
22774
|
+
return { tag: json2.tag_name, assets };
|
|
22775
|
+
} catch (err) {
|
|
22776
|
+
if (signal?.aborted) {
|
|
22777
|
+
warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: aborted`);
|
|
22778
|
+
return null;
|
|
22779
|
+
}
|
|
22780
|
+
warn(`[lsp] github release-by-tag ${githubRepo}@${candidate}: ${err}`);
|
|
22781
|
+
} finally {
|
|
22782
|
+
timeout.cleanup();
|
|
22783
|
+
}
|
|
22784
|
+
}
|
|
22785
|
+
return null;
|
|
22786
|
+
}
|
|
22787
|
+
async function resolveTargetTag(spec, config2, fetchImpl, signal) {
|
|
22788
|
+
const pinned = config2.versions[spec.githubRepo];
|
|
22789
|
+
if (pinned) {
|
|
22790
|
+
try {
|
|
22791
|
+
assertSafeVersion(pinned);
|
|
22792
|
+
} catch (err) {
|
|
22793
|
+
return {
|
|
22794
|
+
tag: null,
|
|
22795
|
+
assets: [],
|
|
22796
|
+
blockedByGrace: false,
|
|
22797
|
+
reason: `invalid pinned version ${JSON.stringify(pinned)}: ${err instanceof Error ? err.message : String(err)}`
|
|
22798
|
+
};
|
|
22799
|
+
}
|
|
22800
|
+
const release = await fetchReleaseByTag(spec.githubRepo, pinned, fetchImpl, signal);
|
|
22801
|
+
if (release) {
|
|
22802
|
+
return {
|
|
22803
|
+
tag: release.tag,
|
|
22804
|
+
assets: release.assets,
|
|
22805
|
+
blockedByGrace: false
|
|
22806
|
+
};
|
|
22807
|
+
}
|
|
22808
|
+
return {
|
|
22809
|
+
tag: null,
|
|
22810
|
+
assets: [],
|
|
22811
|
+
blockedByGrace: false,
|
|
22812
|
+
reason: `pinned tag ${pinned} not found on GitHub`
|
|
22813
|
+
};
|
|
22814
|
+
}
|
|
22815
|
+
const cached2 = readVersionCheck(spec.githubRepo);
|
|
22816
|
+
const weeklyMs = config2.graceDays * 24 * 60 * 60 * 1000;
|
|
22817
|
+
if (!shouldRecheckVersion(cached2, weeklyMs) && cached2?.latest_eligible) {
|
|
22818
|
+
const release = await fetchReleaseByTag(spec.githubRepo, cached2.latest_eligible, fetchImpl);
|
|
22819
|
+
if (release) {
|
|
22820
|
+
return {
|
|
22821
|
+
tag: release.tag,
|
|
22822
|
+
assets: release.assets,
|
|
22823
|
+
blockedByGrace: false
|
|
22824
|
+
};
|
|
22825
|
+
}
|
|
22826
|
+
}
|
|
22827
|
+
const probe = await probeGithubReleases(spec.githubRepo, config2.graceDays, fetchImpl);
|
|
22828
|
+
if (!probe) {
|
|
22829
|
+
return {
|
|
22830
|
+
tag: null,
|
|
22831
|
+
assets: [],
|
|
22832
|
+
blockedByGrace: false,
|
|
22833
|
+
reason: "github releases probe failed"
|
|
22834
|
+
};
|
|
22835
|
+
}
|
|
22836
|
+
writeVersionCheck(spec.githubRepo, probe.tag);
|
|
22837
|
+
return { tag: probe.tag, assets: probe.assets, blockedByGrace: probe.blockedByGrace };
|
|
22838
|
+
}
|
|
22839
|
+
function controlledTimeoutSignal(timeoutMs, parent) {
|
|
22840
|
+
const controller = new AbortController;
|
|
22841
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
22842
|
+
timeout.unref?.();
|
|
22843
|
+
const abort = () => controller.abort();
|
|
22844
|
+
parent?.addEventListener("abort", abort, { once: true });
|
|
22845
|
+
if (parent?.aborted)
|
|
22846
|
+
abort();
|
|
22847
|
+
return {
|
|
22848
|
+
signal: controller.signal,
|
|
22849
|
+
cleanup: () => {
|
|
22850
|
+
clearTimeout(timeout);
|
|
22851
|
+
parent?.removeEventListener("abort", abort);
|
|
22852
|
+
}
|
|
22853
|
+
};
|
|
22854
|
+
}
|
|
22855
|
+
async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
|
|
22856
|
+
if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES) {
|
|
22857
|
+
throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES} (set lsp.versions to pin a smaller release if this is wrong)`);
|
|
22858
|
+
}
|
|
22859
|
+
const timeout = controlledTimeoutSignal(120000, signal);
|
|
22860
|
+
try {
|
|
22861
|
+
const res = await fetchImpl(url2, {
|
|
22862
|
+
headers: { accept: "application/octet-stream" },
|
|
22863
|
+
redirect: "follow",
|
|
22864
|
+
signal: timeout.signal
|
|
22865
|
+
});
|
|
22866
|
+
if (!res.ok || !res.body) {
|
|
22867
|
+
throw new Error(`download failed (${res.status})`);
|
|
22868
|
+
}
|
|
22869
|
+
const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
|
|
22870
|
+
if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES) {
|
|
22871
|
+
throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES}`);
|
|
22872
|
+
}
|
|
22873
|
+
mkdirSync3(dirname(destPath), { recursive: true });
|
|
22874
|
+
let bytesWritten = 0;
|
|
22875
|
+
const guard = new TransformStream({
|
|
22876
|
+
transform(chunk, controller) {
|
|
22877
|
+
bytesWritten += chunk.byteLength;
|
|
22878
|
+
if (bytesWritten > MAX_DOWNLOAD_BYTES) {
|
|
22879
|
+
controller.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES} bytes after streaming (server lied about size or sent unbounded body)`));
|
|
22880
|
+
return;
|
|
22881
|
+
}
|
|
22882
|
+
controller.enqueue(chunk);
|
|
22883
|
+
}
|
|
22884
|
+
});
|
|
22885
|
+
const guarded = res.body.pipeThrough(guard);
|
|
22886
|
+
const nodeStream = Readable.fromWeb(guarded);
|
|
22887
|
+
await pipeline(nodeStream, createWriteStream(destPath), { signal: timeout.signal });
|
|
22888
|
+
} catch (err) {
|
|
22889
|
+
try {
|
|
22890
|
+
unlinkSync3(destPath);
|
|
22891
|
+
} catch {}
|
|
22892
|
+
throw err;
|
|
22893
|
+
} finally {
|
|
22894
|
+
timeout.cleanup();
|
|
22895
|
+
}
|
|
22896
|
+
}
|
|
22897
|
+
function validateExtraction(stagingRoot) {
|
|
22898
|
+
const realStagingRoot = realpathSync(stagingRoot);
|
|
22899
|
+
let totalBytes = 0;
|
|
22900
|
+
const walk = (dir) => {
|
|
22901
|
+
let entries;
|
|
22902
|
+
try {
|
|
22903
|
+
entries = readdirSync2(dir);
|
|
22904
|
+
} catch (err) {
|
|
22905
|
+
throw new Error(`failed to read staging dir ${dir}: ${err}`);
|
|
22906
|
+
}
|
|
22907
|
+
for (const entry of entries) {
|
|
22908
|
+
const full = join6(dir, entry);
|
|
22909
|
+
let lst;
|
|
22910
|
+
try {
|
|
22911
|
+
lst = lstatSync(full);
|
|
22912
|
+
} catch (err) {
|
|
22913
|
+
throw new Error(`failed to lstat ${full}: ${err}`);
|
|
22914
|
+
}
|
|
22915
|
+
if (lst.isSymbolicLink()) {
|
|
22916
|
+
let target = "<unreadable>";
|
|
22917
|
+
try {
|
|
22918
|
+
target = readlinkSync(full);
|
|
22919
|
+
} catch {}
|
|
22920
|
+
throw new Error(`archive contains symlink ${relative(realStagingRoot, full)} \u2192 ${target}; rejecting (zip-slip defense)`);
|
|
22921
|
+
}
|
|
22922
|
+
let realFull;
|
|
22923
|
+
try {
|
|
22924
|
+
realFull = realpathSync(full);
|
|
22925
|
+
} catch (err) {
|
|
22926
|
+
throw new Error(`failed to realpath ${full}: ${err}`);
|
|
22927
|
+
}
|
|
22928
|
+
const rel = relative(realStagingRoot, realFull);
|
|
22929
|
+
if (rel.startsWith("..") || resolve(realStagingRoot, rel) !== realFull) {
|
|
22930
|
+
throw new Error(`archive entry escapes staging root: ${full} \u2192 ${realFull} (zip-slip defense)`);
|
|
22931
|
+
}
|
|
22932
|
+
if (lst.isDirectory()) {
|
|
22933
|
+
walk(full);
|
|
22934
|
+
} else if (lst.isFile()) {
|
|
22935
|
+
totalBytes += lst.size;
|
|
22936
|
+
if (totalBytes > MAX_EXTRACT_BYTES) {
|
|
22937
|
+
throw new Error(`extracted archive exceeds ${MAX_EXTRACT_BYTES} bytes (decompression bomb defense): saw ${totalBytes} bytes before hitting the cap`);
|
|
22938
|
+
}
|
|
22939
|
+
} else {
|
|
22940
|
+
throw new Error(`archive contains non-file/non-dir entry: ${full}`);
|
|
22941
|
+
}
|
|
22942
|
+
}
|
|
22943
|
+
};
|
|
22944
|
+
walk(realStagingRoot);
|
|
22945
|
+
}
|
|
22946
|
+
function extractArchiveSafely(archivePath, destDir, archiveType) {
|
|
22947
|
+
const suffix = randomBytes(8).toString("hex");
|
|
22948
|
+
const stagingDir = `${destDir}.staging-${suffix}`;
|
|
22949
|
+
try {
|
|
22950
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
22951
|
+
} catch {}
|
|
22952
|
+
mkdirSync3(stagingDir, { recursive: true });
|
|
22953
|
+
try {
|
|
22954
|
+
runPlatformExtractor(archivePath, stagingDir, archiveType);
|
|
22955
|
+
validateExtraction(stagingDir);
|
|
22956
|
+
try {
|
|
22957
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
22958
|
+
} catch {}
|
|
22959
|
+
renameSync(stagingDir, destDir);
|
|
22960
|
+
} catch (err) {
|
|
22961
|
+
try {
|
|
22962
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
22963
|
+
} catch {}
|
|
22964
|
+
throw err;
|
|
22965
|
+
}
|
|
22966
|
+
}
|
|
22967
|
+
function runPlatformExtractor(archivePath, destDir, archiveType) {
|
|
22968
|
+
if (archiveType === "zip") {
|
|
22969
|
+
if (process.platform === "win32") {
|
|
22970
|
+
execFileSync("tar.exe", ["-xf", archivePath, "-C", destDir], {
|
|
22971
|
+
stdio: "pipe",
|
|
22972
|
+
timeout: 180000
|
|
22973
|
+
});
|
|
22974
|
+
return;
|
|
22975
|
+
}
|
|
22976
|
+
execFileSync("unzip", ["-q", "-o", archivePath, "-d", destDir], {
|
|
22977
|
+
stdio: "pipe",
|
|
22978
|
+
timeout: 180000
|
|
22979
|
+
});
|
|
22980
|
+
return;
|
|
22981
|
+
}
|
|
22982
|
+
if (archiveType === "tar.gz") {
|
|
22983
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", destDir], {
|
|
22984
|
+
stdio: "pipe",
|
|
22985
|
+
timeout: 180000
|
|
22986
|
+
});
|
|
22987
|
+
return;
|
|
22988
|
+
}
|
|
22989
|
+
if (archiveType === "tar.xz") {
|
|
22990
|
+
execFileSync("tar", ["-xf", archivePath, "-C", destDir], {
|
|
22991
|
+
stdio: "pipe",
|
|
22992
|
+
timeout: 180000
|
|
22993
|
+
});
|
|
22994
|
+
return;
|
|
22995
|
+
}
|
|
22996
|
+
throw new Error(`unsupported archive type: ${archiveType}`);
|
|
22997
|
+
}
|
|
22998
|
+
async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl, signal) {
|
|
22999
|
+
const version2 = stripTagV(tag);
|
|
23000
|
+
const expected = spec.resolveAsset(platform, arch, version2);
|
|
23001
|
+
if (!expected) {
|
|
23002
|
+
warn(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform}/${arch}`);
|
|
23003
|
+
return null;
|
|
23004
|
+
}
|
|
23005
|
+
const matchingAsset = assets.find((a) => a.name === expected.name);
|
|
23006
|
+
if (!matchingAsset) {
|
|
23007
|
+
warn(`[lsp] ${spec.id}: asset ${expected.name} not found in release ${tag} (${assets.length} assets available)`);
|
|
23008
|
+
return null;
|
|
23009
|
+
}
|
|
23010
|
+
const pkgDir = ghPackageDir(spec);
|
|
23011
|
+
const extractDir = ghExtractDir(spec);
|
|
23012
|
+
const archivePath = join6(pkgDir, expected.name);
|
|
23013
|
+
log(`[lsp] downloading ${spec.id} ${tag} \u2192 ${matchingAsset.url}`);
|
|
23014
|
+
try {
|
|
23015
|
+
await downloadFile(matchingAsset.url, archivePath, fetchImpl, matchingAsset.size, signal);
|
|
23016
|
+
} catch (err) {
|
|
23017
|
+
error48(`[lsp] download ${spec.id} failed: ${err}`);
|
|
23018
|
+
return null;
|
|
23019
|
+
}
|
|
23020
|
+
let archiveSha256;
|
|
23021
|
+
try {
|
|
23022
|
+
archiveSha256 = await sha256OfFile(archivePath);
|
|
23023
|
+
} catch (err) {
|
|
23024
|
+
error48(`[lsp] hash ${spec.id} failed: ${err}`);
|
|
23025
|
+
try {
|
|
23026
|
+
unlinkSync3(archivePath);
|
|
23027
|
+
} catch {}
|
|
23028
|
+
return null;
|
|
23029
|
+
}
|
|
23030
|
+
log(`[lsp] ${spec.id} ${tag} sha256=${archiveSha256}`);
|
|
23031
|
+
const previousMeta = readInstalledMetaIn(ghPackageDir(spec));
|
|
23032
|
+
if (previousMeta && previousMeta.version === tag && previousMeta.sha256) {
|
|
23033
|
+
if (previousMeta.sha256 !== archiveSha256) {
|
|
23034
|
+
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.`);
|
|
23035
|
+
try {
|
|
23036
|
+
unlinkSync3(archivePath);
|
|
23037
|
+
} catch {}
|
|
23038
|
+
return null;
|
|
23039
|
+
}
|
|
23040
|
+
}
|
|
23041
|
+
try {
|
|
23042
|
+
extractArchiveSafely(archivePath, extractDir, expected.archive);
|
|
23043
|
+
} catch (err) {
|
|
23044
|
+
error48(`[lsp] extract ${spec.id} failed: ${err}`);
|
|
23045
|
+
return null;
|
|
23046
|
+
} finally {
|
|
23047
|
+
try {
|
|
23048
|
+
unlinkSync3(archivePath);
|
|
23049
|
+
} catch {}
|
|
23050
|
+
}
|
|
23051
|
+
const innerBinaryPath = join6(extractDir, spec.binaryPathInArchive(platform, arch, version2));
|
|
23052
|
+
if (!existsSync5(innerBinaryPath)) {
|
|
23053
|
+
error48(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
|
|
23054
|
+
return null;
|
|
23055
|
+
}
|
|
23056
|
+
const targetBinary = ghBinaryPath(spec, platform);
|
|
23057
|
+
mkdirSync3(dirname(targetBinary), { recursive: true });
|
|
23058
|
+
try {
|
|
23059
|
+
copyFileSync(innerBinaryPath, targetBinary);
|
|
23060
|
+
if (platform !== "win32") {
|
|
23061
|
+
const { chmodSync: chmodSync2 } = await import("fs");
|
|
23062
|
+
chmodSync2(targetBinary, 493);
|
|
23063
|
+
}
|
|
23064
|
+
} catch (err) {
|
|
23065
|
+
error48(`[lsp] ${spec.id}: failed to place binary at ${targetBinary}: ${err}`);
|
|
23066
|
+
return null;
|
|
23067
|
+
}
|
|
23068
|
+
log(`[lsp] installed ${spec.id} ${tag} at ${targetBinary}`);
|
|
23069
|
+
return archiveSha256;
|
|
23070
|
+
}
|
|
23071
|
+
async function ensureGithubInstalled(spec, config2, fetchImpl, platform, arch, signal) {
|
|
23072
|
+
const outcome = await withInstallLock(spec.githubRepo, async () => {
|
|
23073
|
+
const { tag, assets, blockedByGrace, reason } = await resolveTargetTag(spec, config2, fetchImpl, signal);
|
|
23074
|
+
if (!tag) {
|
|
23075
|
+
const installed = isGithubInstalled(spec, platform);
|
|
23076
|
+
if (installed) {
|
|
23077
|
+
warn(`[lsp] no eligible release of ${spec.githubRepo} (grace=${config2.graceDays}d); keeping existing install`);
|
|
23078
|
+
return { started: false, reason: "kept existing install" };
|
|
23079
|
+
}
|
|
23080
|
+
const fallbackReason = reason ?? (blockedByGrace ? `all releases are within ${config2.graceDays}-day grace window` : "github releases probe failed");
|
|
23081
|
+
warn(`[lsp] skipping ${spec.id}: ${fallbackReason}`);
|
|
23082
|
+
return { started: false, reason: fallbackReason };
|
|
23083
|
+
}
|
|
23084
|
+
if (isGithubInstalled(spec, platform)) {
|
|
23085
|
+
const installedMeta = readInstalledMetaIn(ghPackageDir(spec));
|
|
23086
|
+
if (installedMeta && installedMeta.version === tag) {
|
|
23087
|
+
return { started: false, reason: "already installed" };
|
|
23088
|
+
}
|
|
23089
|
+
if (installedMeta) {
|
|
23090
|
+
log(`[lsp] reinstalling ${spec.id}: cached ${installedMeta.version} \u2260 target ${tag}`);
|
|
23091
|
+
} else {
|
|
23092
|
+
log(`[lsp] reinstalling ${spec.id}@${tag}: no installed-version metadata recorded`);
|
|
23093
|
+
}
|
|
23094
|
+
}
|
|
23095
|
+
const archiveSha256 = await downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl, signal).catch((err) => {
|
|
23096
|
+
error48(`[lsp] github install ${spec.id} crashed: ${err}`);
|
|
23097
|
+
return null;
|
|
23098
|
+
});
|
|
23099
|
+
if (!archiveSha256) {
|
|
23100
|
+
return { started: true, reason: "install failed (see plugin log)" };
|
|
23101
|
+
}
|
|
23102
|
+
writeInstalledMetaIn(ghPackageDir(spec), tag, archiveSha256);
|
|
23103
|
+
return { started: true };
|
|
23104
|
+
});
|
|
23105
|
+
if (outcome === null) {
|
|
23106
|
+
return { started: false, reason: "another install in progress" };
|
|
23107
|
+
}
|
|
23108
|
+
return outcome;
|
|
23109
|
+
}
|
|
23110
|
+
var inFlightGithubInstalls = new Set;
|
|
23111
|
+
function trackInFlightGithubInstall(controller, promise2) {
|
|
23112
|
+
const entry = { controller, promise: promise2 };
|
|
23113
|
+
inFlightGithubInstalls.add(entry);
|
|
23114
|
+
promise2.then(() => inFlightGithubInstalls.delete(entry), () => inFlightGithubInstalls.delete(entry));
|
|
23115
|
+
return promise2;
|
|
23116
|
+
}
|
|
23117
|
+
async function abortInFlightGithubInstalls() {
|
|
23118
|
+
const installs = Array.from(inFlightGithubInstalls);
|
|
23119
|
+
for (const install of installs) {
|
|
23120
|
+
install.controller.abort();
|
|
23121
|
+
}
|
|
23122
|
+
await Promise.allSettled(installs.map((install) => install.promise));
|
|
23123
|
+
}
|
|
23124
|
+
function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
|
|
23125
|
+
const cachedBinDirs = [];
|
|
23126
|
+
const skipped = [];
|
|
23127
|
+
const installPromises = [];
|
|
23128
|
+
let installsStarted = 0;
|
|
23129
|
+
const host = detectHostPlatform();
|
|
23130
|
+
if (!host) {
|
|
23131
|
+
for (const spec of GITHUB_LSP_TABLE) {
|
|
23132
|
+
try {
|
|
23133
|
+
if (existsSync5(ghBinDir(spec))) {
|
|
23134
|
+
cachedBinDirs.push(ghBinDir(spec));
|
|
23135
|
+
}
|
|
23136
|
+
} catch {}
|
|
23137
|
+
}
|
|
23138
|
+
return {
|
|
23139
|
+
cachedBinDirs,
|
|
23140
|
+
installsStarted: 0,
|
|
23141
|
+
skipped,
|
|
23142
|
+
installsComplete: Promise.resolve()
|
|
23143
|
+
};
|
|
23144
|
+
}
|
|
23145
|
+
for (const spec of GITHUB_LSP_TABLE) {
|
|
23146
|
+
if (isGithubInstalled(spec, host.platform)) {
|
|
23147
|
+
cachedBinDirs.push(ghBinDir(spec));
|
|
23148
|
+
}
|
|
23149
|
+
if (config2.disabled.has(spec.id)) {
|
|
23150
|
+
skipped.push({ id: spec.id, reason: "disabled by config" });
|
|
23151
|
+
continue;
|
|
23152
|
+
}
|
|
23153
|
+
if (!config2.autoInstall) {
|
|
23154
|
+
skipped.push({ id: spec.id, reason: "auto_install: false" });
|
|
23155
|
+
continue;
|
|
23156
|
+
}
|
|
23157
|
+
if (!relevantServers.has(spec.id)) {
|
|
23158
|
+
skipped.push({ id: spec.id, reason: "not relevant to project" });
|
|
23159
|
+
continue;
|
|
23160
|
+
}
|
|
23161
|
+
installsStarted += 1;
|
|
23162
|
+
const controller = new AbortController;
|
|
23163
|
+
const promise2 = ensureGithubInstalled(spec, config2, fetchImpl, host.platform, host.arch, controller.signal).then((outcome) => {
|
|
23164
|
+
if (!outcome.started)
|
|
23165
|
+
installsStarted -= 1;
|
|
23166
|
+
if (outcome.reason && outcome.reason !== "already installed") {
|
|
23167
|
+
skipped.push({ id: spec.id, reason: outcome.reason });
|
|
23168
|
+
}
|
|
23169
|
+
}, (err) => {
|
|
23170
|
+
installsStarted -= 1;
|
|
23171
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
23172
|
+
skipped.push({ id: spec.id, reason: `install error: ${reason}` });
|
|
23173
|
+
error48(`[lsp] github install ${spec.id} promise rejected: ${reason}`);
|
|
23174
|
+
});
|
|
23175
|
+
installPromises.push(trackInFlightGithubInstall(controller, promise2));
|
|
23176
|
+
}
|
|
23177
|
+
return {
|
|
23178
|
+
cachedBinDirs,
|
|
23179
|
+
get installsStarted() {
|
|
23180
|
+
return installsStarted;
|
|
23181
|
+
},
|
|
23182
|
+
skipped,
|
|
23183
|
+
installsComplete: Promise.all(installPromises).then(() => {})
|
|
23184
|
+
};
|
|
23185
|
+
}
|
|
23186
|
+
function discoverRelevantGithubServers(projectRoot) {
|
|
23187
|
+
const extToServerIds = {
|
|
23188
|
+
c: ["clangd"],
|
|
23189
|
+
"c++": ["clangd"],
|
|
23190
|
+
cc: ["clangd"],
|
|
23191
|
+
cpp: ["clangd"],
|
|
23192
|
+
cxx: ["clangd"],
|
|
23193
|
+
h: ["clangd"],
|
|
23194
|
+
"h++": ["clangd"],
|
|
23195
|
+
hpp: ["clangd"],
|
|
23196
|
+
hh: ["clangd"],
|
|
23197
|
+
hxx: ["clangd"],
|
|
23198
|
+
lua: ["lua-ls"],
|
|
23199
|
+
zig: ["zls"],
|
|
23200
|
+
zon: ["zls"],
|
|
23201
|
+
typ: ["tinymist"],
|
|
23202
|
+
typc: ["tinymist"],
|
|
23203
|
+
tex: ["texlab"],
|
|
23204
|
+
bib: ["texlab"]
|
|
23205
|
+
};
|
|
23206
|
+
const rootMarkers = {
|
|
23207
|
+
clangd: ["compile_commands.json", "compile_flags.txt", ".clangd"],
|
|
23208
|
+
"lua-ls": [".luarc.json", ".luarc.jsonc", ".stylua.toml", "stylua.toml"],
|
|
23209
|
+
zls: ["build.zig"],
|
|
23210
|
+
tinymist: ["typst.toml"],
|
|
23211
|
+
texlab: [".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]
|
|
23212
|
+
};
|
|
23213
|
+
const relevant = new Set;
|
|
23214
|
+
for (const spec of GITHUB_LSP_TABLE) {
|
|
23215
|
+
if (hasRootMarker(projectRoot, rootMarkers[spec.id]))
|
|
23216
|
+
relevant.add(spec.id);
|
|
23217
|
+
}
|
|
23218
|
+
const extensions = relevantExtensionsInProject(projectRoot, extToServerIds);
|
|
23219
|
+
for (const ext of extensions) {
|
|
23220
|
+
for (const id of extToServerIds[ext] ?? []) {
|
|
23221
|
+
relevant.add(id);
|
|
23222
|
+
}
|
|
23223
|
+
}
|
|
23224
|
+
return relevant;
|
|
23225
|
+
}
|
|
23226
|
+
|
|
21808
23227
|
// src/metadata-store.ts
|
|
21809
23228
|
var pendingStore = new Map;
|
|
21810
23229
|
var STALE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
@@ -21871,9 +23290,9 @@ function normalizeToolMap(tools) {
|
|
|
21871
23290
|
}
|
|
21872
23291
|
|
|
21873
23292
|
// src/notifications.ts
|
|
21874
|
-
import { existsSync as
|
|
21875
|
-
import { homedir as
|
|
21876
|
-
import { join as
|
|
23293
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
23294
|
+
import { homedir as homedir4, platform } from "os";
|
|
23295
|
+
import { join as join7 } from "path";
|
|
21877
23296
|
function isTuiMode() {
|
|
21878
23297
|
return process.env.OPENCODE_CLIENT === "cli";
|
|
21879
23298
|
}
|
|
@@ -21895,27 +23314,27 @@ var STATUS_MARKER = `${AFT_MARKER} \u2705`;
|
|
|
21895
23314
|
var WARNED_TOOLS_FILE = "warned_tools.json";
|
|
21896
23315
|
function getDesktopStatePath() {
|
|
21897
23316
|
const os2 = platform();
|
|
21898
|
-
const home =
|
|
23317
|
+
const home = homedir4();
|
|
21899
23318
|
if (os2 === "darwin") {
|
|
21900
|
-
return
|
|
23319
|
+
return join7(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
|
|
21901
23320
|
}
|
|
21902
23321
|
if (os2 === "linux") {
|
|
21903
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ||
|
|
21904
|
-
return
|
|
23322
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join7(home, ".config");
|
|
23323
|
+
return join7(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
|
|
21905
23324
|
}
|
|
21906
23325
|
if (os2 === "win32") {
|
|
21907
|
-
const appData = process.env.APPDATA ||
|
|
21908
|
-
return
|
|
23326
|
+
const appData = process.env.APPDATA || join7(home, "AppData", "Roaming");
|
|
23327
|
+
return join7(appData, "ai.opencode.desktop", "opencode.global.dat");
|
|
21909
23328
|
}
|
|
21910
23329
|
return null;
|
|
21911
23330
|
}
|
|
21912
23331
|
function readDesktopState(directory) {
|
|
21913
23332
|
const statePath = getDesktopStatePath();
|
|
21914
|
-
if (!statePath || !
|
|
23333
|
+
if (!statePath || !existsSync6(statePath)) {
|
|
21915
23334
|
return { sessionId: null, serverUrl: null };
|
|
21916
23335
|
}
|
|
21917
23336
|
try {
|
|
21918
|
-
const raw =
|
|
23337
|
+
const raw = readFileSync3(statePath, "utf-8");
|
|
21919
23338
|
const state = JSON.parse(raw);
|
|
21920
23339
|
let serverUrl = null;
|
|
21921
23340
|
const serverStr = state.server;
|
|
@@ -22009,10 +23428,10 @@ async function sendWarning(opts, message) {
|
|
|
22009
23428
|
}
|
|
22010
23429
|
async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
|
|
22011
23430
|
if (storageDir) {
|
|
22012
|
-
const versionFile =
|
|
23431
|
+
const versionFile = join7(storageDir, "last_announced_version");
|
|
22013
23432
|
try {
|
|
22014
|
-
if (
|
|
22015
|
-
const lastVersion =
|
|
23433
|
+
if (existsSync6(versionFile)) {
|
|
23434
|
+
const lastVersion = readFileSync3(versionFile, "utf-8").trim();
|
|
22016
23435
|
if (lastVersion === version2)
|
|
22017
23436
|
return;
|
|
22018
23437
|
}
|
|
@@ -22032,17 +23451,17 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
|
|
|
22032
23451
|
}
|
|
22033
23452
|
if (storageDir) {
|
|
22034
23453
|
try {
|
|
22035
|
-
|
|
22036
|
-
|
|
23454
|
+
mkdirSync4(storageDir, { recursive: true });
|
|
23455
|
+
writeFileSync2(join7(storageDir, "last_announced_version"), version2);
|
|
22037
23456
|
} catch {}
|
|
22038
23457
|
}
|
|
22039
23458
|
}
|
|
22040
23459
|
function readWarnedTools(storageDir) {
|
|
22041
23460
|
try {
|
|
22042
|
-
const warnedToolsPath =
|
|
22043
|
-
if (!
|
|
23461
|
+
const warnedToolsPath = join7(storageDir, WARNED_TOOLS_FILE);
|
|
23462
|
+
if (!existsSync6(warnedToolsPath))
|
|
22044
23463
|
return {};
|
|
22045
|
-
const parsed = JSON.parse(
|
|
23464
|
+
const parsed = JSON.parse(readFileSync3(warnedToolsPath, "utf-8"));
|
|
22046
23465
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
22047
23466
|
return {};
|
|
22048
23467
|
const warned = {};
|
|
@@ -22058,9 +23477,9 @@ function readWarnedTools(storageDir) {
|
|
|
22058
23477
|
}
|
|
22059
23478
|
function writeWarnedTools(storageDir, warned) {
|
|
22060
23479
|
try {
|
|
22061
|
-
|
|
22062
|
-
const warnedToolsPath =
|
|
22063
|
-
|
|
23480
|
+
mkdirSync4(storageDir, { recursive: true });
|
|
23481
|
+
const warnedToolsPath = join7(storageDir, WARNED_TOOLS_FILE);
|
|
23482
|
+
writeFileSync2(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
|
|
22064
23483
|
`);
|
|
22065
23484
|
} catch {}
|
|
22066
23485
|
}
|
|
@@ -22151,8 +23570,8 @@ async function cleanupWarnings(opts) {
|
|
|
22151
23570
|
}
|
|
22152
23571
|
|
|
22153
23572
|
// src/onnx-runtime.ts
|
|
22154
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
22155
|
-
import { join as
|
|
23573
|
+
import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
23574
|
+
import { join as join8 } from "path";
|
|
22156
23575
|
var ORT_VERSION = "1.24.4";
|
|
22157
23576
|
var ORT_REPO = "microsoft/onnxruntime";
|
|
22158
23577
|
var ORT_PLATFORM_MAP = {
|
|
@@ -22208,9 +23627,9 @@ function getManualInstallHint() {
|
|
|
22208
23627
|
}
|
|
22209
23628
|
async function ensureOnnxRuntime(storageDir) {
|
|
22210
23629
|
const info = getPlatformInfo();
|
|
22211
|
-
const ortDir =
|
|
22212
|
-
const libPath =
|
|
22213
|
-
if (
|
|
23630
|
+
const ortDir = join8(storageDir, "onnxruntime", ORT_VERSION);
|
|
23631
|
+
const libPath = join8(ortDir, info?.libName ?? "libonnxruntime.dylib");
|
|
23632
|
+
if (existsSync7(libPath)) {
|
|
22214
23633
|
log(`ONNX Runtime found at ${ortDir}`);
|
|
22215
23634
|
return ortDir;
|
|
22216
23635
|
}
|
|
@@ -22235,7 +23654,7 @@ function findSystemOnnxRuntime(libName) {
|
|
|
22235
23654
|
searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
|
|
22236
23655
|
}
|
|
22237
23656
|
for (const dir of searchPaths) {
|
|
22238
|
-
if (
|
|
23657
|
+
if (existsSync7(join8(dir, libName))) {
|
|
22239
23658
|
return dir;
|
|
22240
23659
|
}
|
|
22241
23660
|
}
|
|
@@ -22246,34 +23665,34 @@ async function downloadOnnxRuntime(info, targetDir) {
|
|
|
22246
23665
|
log(`Downloading ONNX Runtime v${ORT_VERSION} for ${process.platform}/${process.arch}...`);
|
|
22247
23666
|
try {
|
|
22248
23667
|
const tmpDir = `${targetDir}.tmp.${process.pid}`;
|
|
22249
|
-
|
|
22250
|
-
const archivePath =
|
|
22251
|
-
const { execFileSync } = await import("child_process");
|
|
22252
|
-
|
|
23668
|
+
mkdirSync5(tmpDir, { recursive: true });
|
|
23669
|
+
const archivePath = join8(tmpDir, `onnxruntime.${info.archiveType}`);
|
|
23670
|
+
const { execFileSync: execFileSync2 } = await import("child_process");
|
|
23671
|
+
execFileSync2("curl", ["-fsSL", url2, "-o", archivePath], {
|
|
22253
23672
|
stdio: "pipe",
|
|
22254
23673
|
timeout: 120000
|
|
22255
23674
|
});
|
|
22256
23675
|
if (info.archiveType === "tgz") {
|
|
22257
|
-
|
|
23676
|
+
execFileSync2("tar", ["xzf", archivePath, "-C", tmpDir], { stdio: "pipe" });
|
|
22258
23677
|
} else {
|
|
22259
23678
|
await extractZipArchive(archivePath, tmpDir);
|
|
22260
23679
|
}
|
|
22261
|
-
const extractedDir =
|
|
22262
|
-
if (!
|
|
23680
|
+
const extractedDir = join8(tmpDir, info.assetName, "lib");
|
|
23681
|
+
if (!existsSync7(extractedDir)) {
|
|
22263
23682
|
throw new Error(`Expected directory not found: ${extractedDir}`);
|
|
22264
23683
|
}
|
|
22265
|
-
|
|
22266
|
-
const libFiles =
|
|
22267
|
-
const { lstatSync, symlinkSync, readlinkSync, copyFileSync: cpFile } = await import("fs");
|
|
23684
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
23685
|
+
const libFiles = readdirSync3(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
|
|
23686
|
+
const { lstatSync: lstatSync2, symlinkSync, readlinkSync: readlinkSync2, copyFileSync: cpFile } = await import("fs");
|
|
22268
23687
|
const realFiles = [];
|
|
22269
23688
|
const symlinks = [];
|
|
22270
23689
|
for (const libFile of libFiles) {
|
|
22271
|
-
const src =
|
|
23690
|
+
const src = join8(extractedDir, libFile);
|
|
22272
23691
|
try {
|
|
22273
|
-
const stat =
|
|
23692
|
+
const stat = lstatSync2(src);
|
|
22274
23693
|
log(`ORT extract: ${libFile} \u2014 isSymlink=${stat.isSymbolicLink()}, isFile=${stat.isFile()}, size=${stat.size}`);
|
|
22275
23694
|
if (stat.isSymbolicLink()) {
|
|
22276
|
-
symlinks.push({ name: libFile, target:
|
|
23695
|
+
symlinks.push({ name: libFile, target: readlinkSync2(src) });
|
|
22277
23696
|
} else {
|
|
22278
23697
|
realFiles.push(libFile);
|
|
22279
23698
|
}
|
|
@@ -22283,8 +23702,8 @@ async function downloadOnnxRuntime(info, targetDir) {
|
|
|
22283
23702
|
}
|
|
22284
23703
|
}
|
|
22285
23704
|
for (const libFile of realFiles) {
|
|
22286
|
-
const src =
|
|
22287
|
-
const dst =
|
|
23705
|
+
const src = join8(extractedDir, libFile);
|
|
23706
|
+
const dst = join8(targetDir, libFile);
|
|
22288
23707
|
try {
|
|
22289
23708
|
cpFile(src, dst);
|
|
22290
23709
|
if (process.platform !== "win32") {
|
|
@@ -22295,62 +23714,44 @@ async function downloadOnnxRuntime(info, targetDir) {
|
|
|
22295
23714
|
}
|
|
22296
23715
|
}
|
|
22297
23716
|
for (const link of symlinks) {
|
|
22298
|
-
const dst =
|
|
23717
|
+
const dst = join8(targetDir, link.name);
|
|
22299
23718
|
try {
|
|
22300
|
-
|
|
23719
|
+
unlinkSync4(dst);
|
|
22301
23720
|
} catch {}
|
|
22302
23721
|
symlinkSync(link.target, dst);
|
|
22303
23722
|
}
|
|
22304
|
-
const { rmSync } = await import("fs");
|
|
22305
|
-
|
|
23723
|
+
const { rmSync: rmSync2 } = await import("fs");
|
|
23724
|
+
rmSync2(tmpDir, { recursive: true, force: true });
|
|
22306
23725
|
log(`ONNX Runtime v${ORT_VERSION} installed to ${targetDir}`);
|
|
22307
23726
|
return targetDir;
|
|
22308
23727
|
} catch (err) {
|
|
22309
23728
|
error48(`Failed to download ONNX Runtime: ${err}`);
|
|
22310
23729
|
try {
|
|
22311
|
-
const { rmSync } = await import("fs");
|
|
22312
|
-
|
|
23730
|
+
const { rmSync: rmSync2 } = await import("fs");
|
|
23731
|
+
rmSync2(`${targetDir}.tmp.${process.pid}`, { recursive: true, force: true });
|
|
22313
23732
|
} catch {}
|
|
22314
23733
|
return null;
|
|
22315
23734
|
}
|
|
22316
23735
|
}
|
|
22317
23736
|
async function extractZipArchive(archivePath, destinationDir) {
|
|
22318
|
-
const { execFileSync } = await import("child_process");
|
|
23737
|
+
const { execFileSync: execFileSync2 } = await import("child_process");
|
|
22319
23738
|
if (process.platform === "win32") {
|
|
22320
|
-
|
|
22321
|
-
|
|
22322
|
-
|
|
22323
|
-
|
|
22324
|
-
|
|
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;
|
|
22333
|
-
} catch (err) {
|
|
22334
|
-
powershellError = err;
|
|
22335
|
-
warn(`PowerShell Expand-Archive failed, falling back to cmd/tar: ${String(err)}`);
|
|
22336
|
-
}
|
|
22337
|
-
try {
|
|
22338
|
-
execFileSync("cmd.exe", ["/d", "/s", "/c", `tar -xf "${archivePath}" -C "${destinationDir}"`], { stdio: "pipe", timeout: 120000 });
|
|
22339
|
-
return;
|
|
22340
|
-
} catch (cmdError) {
|
|
22341
|
-
throw new Error(`ZIP extraction failed via PowerShell and cmd/tar. PowerShell: ${String(powershellError)} | cmd/tar: ${String(cmdError)}`);
|
|
22342
|
-
}
|
|
23739
|
+
execFileSync2("tar.exe", ["-xf", archivePath, "-C", destinationDir], {
|
|
23740
|
+
stdio: "pipe",
|
|
23741
|
+
timeout: 120000
|
|
23742
|
+
});
|
|
23743
|
+
return;
|
|
22343
23744
|
}
|
|
22344
|
-
|
|
23745
|
+
execFileSync2("unzip", ["-q", archivePath, "-d", destinationDir], {
|
|
22345
23746
|
stdio: "pipe",
|
|
22346
23747
|
timeout: 120000
|
|
22347
23748
|
});
|
|
22348
23749
|
}
|
|
22349
23750
|
|
|
22350
23751
|
// src/bridge.ts
|
|
22351
|
-
import { spawn } from "child_process";
|
|
22352
|
-
import { homedir as
|
|
22353
|
-
import { join as
|
|
23752
|
+
import { spawn as spawn2 } from "child_process";
|
|
23753
|
+
import { homedir as homedir5 } from "os";
|
|
23754
|
+
import { join as join9 } from "path";
|
|
22354
23755
|
var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
|
|
22355
23756
|
var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
|
|
22356
23757
|
var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
|
|
@@ -22492,14 +23893,14 @@ class BinaryBridge {
|
|
|
22492
23893
|
const line = `${JSON.stringify(request)}
|
|
22493
23894
|
`;
|
|
22494
23895
|
const effectiveTimeoutMs = options?.timeoutMs ?? this.timeoutMs;
|
|
22495
|
-
return new Promise((
|
|
23896
|
+
return new Promise((resolve2, reject) => {
|
|
22496
23897
|
const timer = setTimeout(() => {
|
|
22497
23898
|
this.pending.delete(id);
|
|
22498
23899
|
warn(`Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms \u2014 restarting bridge`);
|
|
22499
23900
|
reject(new Error(`[aft-plugin] Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
|
|
22500
23901
|
this.handleTimeout();
|
|
22501
23902
|
}, effectiveTimeoutMs);
|
|
22502
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
23903
|
+
this.pending.set(id, { resolve: resolve2, reject, timer });
|
|
22503
23904
|
if (!this.process?.stdin?.writable) {
|
|
22504
23905
|
this.pending.delete(id);
|
|
22505
23906
|
clearTimeout(timer);
|
|
@@ -22542,15 +23943,15 @@ class BinaryBridge {
|
|
|
22542
23943
|
if (this.process) {
|
|
22543
23944
|
const proc = this.process;
|
|
22544
23945
|
this.process = null;
|
|
22545
|
-
return new Promise((
|
|
23946
|
+
return new Promise((resolve2) => {
|
|
22546
23947
|
const forceKillTimer = setTimeout(() => {
|
|
22547
23948
|
proc.kill("SIGKILL");
|
|
22548
|
-
|
|
23949
|
+
resolve2();
|
|
22549
23950
|
}, 5000);
|
|
22550
23951
|
proc.once("exit", () => {
|
|
22551
23952
|
clearTimeout(forceKillTimer);
|
|
22552
23953
|
log("Process exited during shutdown");
|
|
22553
|
-
|
|
23954
|
+
resolve2();
|
|
22554
23955
|
});
|
|
22555
23956
|
proc.kill("SIGTERM");
|
|
22556
23957
|
});
|
|
@@ -22592,19 +23993,19 @@ class BinaryBridge {
|
|
|
22592
23993
|
})();
|
|
22593
23994
|
const useFastembedBackend = semanticBackend === undefined || semanticBackend === "fastembed" || semanticBackend === "";
|
|
22594
23995
|
const ortDir = typeof this.configOverrides._ort_dylib_dir === "string" && useFastembedBackend ? this.configOverrides._ort_dylib_dir : null;
|
|
22595
|
-
const ortLibraryPath = ortDir == null ? null :
|
|
23996
|
+
const ortLibraryPath = ortDir == null ? null : join9(ortDir, process.platform === "win32" ? "onnxruntime.dll" : process.platform === "darwin" ? "libonnxruntime.dylib" : "libonnxruntime.so");
|
|
22596
23997
|
const envPath = process.platform === "win32" && ortDir ? `${ortDir};${process.env.PATH ?? ""}` : process.env.PATH;
|
|
22597
23998
|
const env = {
|
|
22598
23999
|
...process.env,
|
|
22599
24000
|
...envPath ? { PATH: envPath } : {}
|
|
22600
24001
|
};
|
|
22601
24002
|
if (useFastembedBackend) {
|
|
22602
|
-
env.FASTEMBED_CACHE_DIR = process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ?
|
|
24003
|
+
env.FASTEMBED_CACHE_DIR = process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join9(this.configOverrides.storage_dir, "semantic", "models") : join9(homedir5() || "", ".cache", "fastembed"));
|
|
22603
24004
|
if (ortLibraryPath) {
|
|
22604
24005
|
env.ORT_DYLIB_PATH = ortLibraryPath;
|
|
22605
24006
|
}
|
|
22606
24007
|
}
|
|
22607
|
-
const child =
|
|
24008
|
+
const child = spawn2(this.binaryPath, [], {
|
|
22608
24009
|
cwd: this.cwd,
|
|
22609
24010
|
stdio: ["pipe", "pipe", "pipe"],
|
|
22610
24011
|
env
|
|
@@ -22865,10 +24266,10 @@ function normalizeKey(projectRoot) {
|
|
|
22865
24266
|
|
|
22866
24267
|
// src/resolver.ts
|
|
22867
24268
|
import { execSync, spawnSync } from "child_process";
|
|
22868
|
-
import { chmodSync as chmodSync3, copyFileSync, existsSync as
|
|
24269
|
+
import { chmodSync as chmodSync3, copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, renameSync as renameSync2 } from "fs";
|
|
22869
24270
|
import { createRequire } from "module";
|
|
22870
|
-
import { homedir as
|
|
22871
|
-
import { join as
|
|
24271
|
+
import { homedir as homedir6 } from "os";
|
|
24272
|
+
import { join as join10 } from "path";
|
|
22872
24273
|
function copyToVersionedCache(npmBinaryPath) {
|
|
22873
24274
|
try {
|
|
22874
24275
|
const result = spawnSync(npmBinaryPath, ["--version"], {
|
|
@@ -22882,18 +24283,18 @@ function copyToVersionedCache(npmBinaryPath) {
|
|
|
22882
24283
|
const version2 = rawVersion.replace(/^aft\s+/, "");
|
|
22883
24284
|
const tag = version2.startsWith("v") ? version2 : `v${version2}`;
|
|
22884
24285
|
const cacheDir = getCacheDir();
|
|
22885
|
-
const versionedDir =
|
|
24286
|
+
const versionedDir = join10(cacheDir, tag);
|
|
22886
24287
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
22887
|
-
const cachedPath =
|
|
22888
|
-
if (
|
|
24288
|
+
const cachedPath = join10(versionedDir, `aft${ext}`);
|
|
24289
|
+
if (existsSync8(cachedPath))
|
|
22889
24290
|
return cachedPath;
|
|
22890
|
-
|
|
24291
|
+
mkdirSync6(versionedDir, { recursive: true });
|
|
22891
24292
|
const tmpPath = `${cachedPath}.tmp`;
|
|
22892
|
-
|
|
24293
|
+
copyFileSync2(npmBinaryPath, tmpPath);
|
|
22893
24294
|
if (process.platform !== "win32") {
|
|
22894
24295
|
chmodSync3(tmpPath, 493);
|
|
22895
24296
|
}
|
|
22896
|
-
|
|
24297
|
+
renameSync2(tmpPath, cachedPath);
|
|
22897
24298
|
log(`Copied npm binary to versioned cache: ${cachedPath}`);
|
|
22898
24299
|
return cachedPath;
|
|
22899
24300
|
} catch (err) {
|
|
@@ -22932,7 +24333,7 @@ function findBinarySync() {
|
|
|
22932
24333
|
const packageBin = `@cortexkit/aft-${key}/bin/aft${ext}`;
|
|
22933
24334
|
const req = createRequire(import.meta.url);
|
|
22934
24335
|
const resolved = req.resolve(packageBin);
|
|
22935
|
-
if (
|
|
24336
|
+
if (existsSync8(resolved)) {
|
|
22936
24337
|
const copied = copyToVersionedCache(resolved);
|
|
22937
24338
|
return copied ?? resolved;
|
|
22938
24339
|
}
|
|
@@ -22946,8 +24347,8 @@ function findBinarySync() {
|
|
|
22946
24347
|
if (result)
|
|
22947
24348
|
return result;
|
|
22948
24349
|
} catch {}
|
|
22949
|
-
const cargoPath =
|
|
22950
|
-
if (
|
|
24350
|
+
const cargoPath = join10(homedir6(), ".cargo", "bin", `aft${ext}`);
|
|
24351
|
+
if (existsSync8(cargoPath))
|
|
22951
24352
|
return cargoPath;
|
|
22952
24353
|
return null;
|
|
22953
24354
|
}
|
|
@@ -22982,21 +24383,21 @@ async function findBinary() {
|
|
|
22982
24383
|
}
|
|
22983
24384
|
|
|
22984
24385
|
// src/shared/rpc-server.ts
|
|
22985
|
-
import { randomBytes } from "crypto";
|
|
22986
|
-
import { mkdirSync as
|
|
24386
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
24387
|
+
import { mkdirSync as mkdirSync7, renameSync as renameSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
22987
24388
|
import { createServer } from "http";
|
|
22988
|
-
import { dirname } from "path";
|
|
24389
|
+
import { dirname as dirname2 } from "path";
|
|
22989
24390
|
|
|
22990
24391
|
// src/shared/rpc-utils.ts
|
|
22991
|
-
import { createHash } from "crypto";
|
|
22992
|
-
import { join as
|
|
24392
|
+
import { createHash as createHash3 } from "crypto";
|
|
24393
|
+
import { join as join11 } from "path";
|
|
22993
24394
|
function projectHash(directory) {
|
|
22994
24395
|
const normalized = directory.replace(/\/+$/, "");
|
|
22995
|
-
return
|
|
24396
|
+
return createHash3("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
22996
24397
|
}
|
|
22997
24398
|
function rpcPortFilePath(storageDir, directory) {
|
|
22998
24399
|
const hash2 = projectHash(directory);
|
|
22999
|
-
return
|
|
24400
|
+
return join11(storageDir, "rpc", hash2, "port");
|
|
23000
24401
|
}
|
|
23001
24402
|
|
|
23002
24403
|
// src/shared/rpc-server.ts
|
|
@@ -23013,7 +24414,7 @@ class AftRpcServer {
|
|
|
23013
24414
|
this.handlers.set(method, handler);
|
|
23014
24415
|
}
|
|
23015
24416
|
async start() {
|
|
23016
|
-
return new Promise((
|
|
24417
|
+
return new Promise((resolve2, reject) => {
|
|
23017
24418
|
const server = createServer((req, res) => this.dispatch(req, res));
|
|
23018
24419
|
server.on("error", (err) => {
|
|
23019
24420
|
warn(`RPC server error: ${err.message}`);
|
|
@@ -23026,19 +24427,19 @@ class AftRpcServer {
|
|
|
23026
24427
|
return;
|
|
23027
24428
|
}
|
|
23028
24429
|
this.port = addr.port;
|
|
23029
|
-
this.token =
|
|
24430
|
+
this.token = randomBytes2(32).toString("hex");
|
|
23030
24431
|
this.server = server;
|
|
23031
24432
|
try {
|
|
23032
|
-
const dir =
|
|
23033
|
-
|
|
24433
|
+
const dir = dirname2(this.portFilePath);
|
|
24434
|
+
mkdirSync7(dir, { recursive: true });
|
|
23034
24435
|
const tmpPath = `${this.portFilePath}.tmp`;
|
|
23035
|
-
|
|
23036
|
-
|
|
24436
|
+
writeFileSync3(tmpPath, JSON.stringify({ port: this.port, token: this.token }), "utf-8");
|
|
24437
|
+
renameSync3(tmpPath, this.portFilePath);
|
|
23037
24438
|
log(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
23038
24439
|
} catch (err) {
|
|
23039
24440
|
warn(`Failed to write RPC port file: ${err}`);
|
|
23040
24441
|
}
|
|
23041
|
-
|
|
24442
|
+
resolve2(this.port);
|
|
23042
24443
|
});
|
|
23043
24444
|
server.unref();
|
|
23044
24445
|
});
|
|
@@ -23050,7 +24451,7 @@ class AftRpcServer {
|
|
|
23050
24451
|
}
|
|
23051
24452
|
this.token = null;
|
|
23052
24453
|
try {
|
|
23053
|
-
|
|
24454
|
+
unlinkSync5(this.portFilePath);
|
|
23054
24455
|
} catch {}
|
|
23055
24456
|
}
|
|
23056
24457
|
dispatch(req, res) {
|
|
@@ -23272,21 +24673,21 @@ function formatStatusMarkdown(status) {
|
|
|
23272
24673
|
|
|
23273
24674
|
// src/shared/tui-config.ts
|
|
23274
24675
|
var import_comment_json2 = __toESM(require_src2(), 1);
|
|
23275
|
-
import { existsSync as
|
|
23276
|
-
import { dirname as
|
|
24676
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
24677
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
23277
24678
|
|
|
23278
24679
|
// src/shared/opencode-config-dir.ts
|
|
23279
|
-
import { homedir as
|
|
23280
|
-
import { join as
|
|
24680
|
+
import { homedir as homedir7 } from "os";
|
|
24681
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
23281
24682
|
function getCliConfigDir() {
|
|
23282
24683
|
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
23283
24684
|
if (envConfigDir) {
|
|
23284
|
-
return
|
|
24685
|
+
return resolve2(envConfigDir);
|
|
23285
24686
|
}
|
|
23286
24687
|
if (process.platform === "win32") {
|
|
23287
|
-
return
|
|
24688
|
+
return join12(homedir7(), ".config", "opencode");
|
|
23288
24689
|
}
|
|
23289
|
-
return
|
|
24690
|
+
return join12(process.env.XDG_CONFIG_HOME || join12(homedir7(), ".config"), "opencode");
|
|
23290
24691
|
}
|
|
23291
24692
|
function getOpenCodeConfigDir2(_options) {
|
|
23292
24693
|
return getCliConfigDir();
|
|
@@ -23295,10 +24696,10 @@ function getOpenCodeConfigPaths(options) {
|
|
|
23295
24696
|
const configDir = getOpenCodeConfigDir2(options);
|
|
23296
24697
|
return {
|
|
23297
24698
|
configDir,
|
|
23298
|
-
configJson:
|
|
23299
|
-
configJsonc:
|
|
23300
|
-
packageJson:
|
|
23301
|
-
omoConfig:
|
|
24699
|
+
configJson: join12(configDir, "opencode.json"),
|
|
24700
|
+
configJsonc: join12(configDir, "opencode.jsonc"),
|
|
24701
|
+
packageJson: join12(configDir, "package.json"),
|
|
24702
|
+
omoConfig: join12(configDir, "magic-context.jsonc")
|
|
23302
24703
|
};
|
|
23303
24704
|
}
|
|
23304
24705
|
|
|
@@ -23307,11 +24708,11 @@ var PLUGIN_NAME = "@cortexkit/aft-opencode";
|
|
|
23307
24708
|
var PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
|
|
23308
24709
|
function resolveTuiConfigPath() {
|
|
23309
24710
|
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
|
|
23310
|
-
const jsoncPath =
|
|
23311
|
-
const jsonPath =
|
|
23312
|
-
if (
|
|
24711
|
+
const jsoncPath = join13(configDir, "tui.jsonc");
|
|
24712
|
+
const jsonPath = join13(configDir, "tui.json");
|
|
24713
|
+
if (existsSync9(jsoncPath))
|
|
23313
24714
|
return jsoncPath;
|
|
23314
|
-
if (
|
|
24715
|
+
if (existsSync9(jsonPath))
|
|
23315
24716
|
return jsonPath;
|
|
23316
24717
|
return jsonPath;
|
|
23317
24718
|
}
|
|
@@ -23319,8 +24720,8 @@ function ensureTuiPluginEntry() {
|
|
|
23319
24720
|
try {
|
|
23320
24721
|
const configPath = resolveTuiConfigPath();
|
|
23321
24722
|
let config2 = {};
|
|
23322
|
-
if (
|
|
23323
|
-
config2 = import_comment_json2.parse(
|
|
24723
|
+
if (existsSync9(configPath)) {
|
|
24724
|
+
config2 = import_comment_json2.parse(readFileSync4(configPath, "utf-8")) ?? {};
|
|
23324
24725
|
}
|
|
23325
24726
|
const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
|
|
23326
24727
|
if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) || plugin.includes("opencode-plugin") || plugin.includes("aft-opencode"))) {
|
|
@@ -23328,8 +24729,8 @@ function ensureTuiPluginEntry() {
|
|
|
23328
24729
|
}
|
|
23329
24730
|
plugins.push(PLUGIN_ENTRY);
|
|
23330
24731
|
config2.plugin = plugins;
|
|
23331
|
-
|
|
23332
|
-
|
|
24732
|
+
mkdirSync8(dirname3(configPath), { recursive: true });
|
|
24733
|
+
writeFileSync4(configPath, `${import_comment_json2.stringify(config2, null, 2)}
|
|
23333
24734
|
`);
|
|
23334
24735
|
log(`[aft-plugin] added TUI plugin entry to ${configPath}`);
|
|
23335
24736
|
return true;
|
|
@@ -23340,33 +24741,33 @@ function ensureTuiPluginEntry() {
|
|
|
23340
24741
|
}
|
|
23341
24742
|
|
|
23342
24743
|
// src/shared/url-fetch.ts
|
|
23343
|
-
import { createHash as
|
|
24744
|
+
import { createHash as createHash4 } from "crypto";
|
|
23344
24745
|
import { lookup } from "dns/promises";
|
|
23345
24746
|
import {
|
|
23346
|
-
existsSync as
|
|
23347
|
-
mkdirSync as
|
|
23348
|
-
readdirSync as
|
|
23349
|
-
readFileSync as
|
|
23350
|
-
unlinkSync as
|
|
23351
|
-
writeFileSync as
|
|
24747
|
+
existsSync as existsSync10,
|
|
24748
|
+
mkdirSync as mkdirSync9,
|
|
24749
|
+
readdirSync as readdirSync4,
|
|
24750
|
+
readFileSync as readFileSync5,
|
|
24751
|
+
unlinkSync as unlinkSync6,
|
|
24752
|
+
writeFileSync as writeFileSync5
|
|
23352
24753
|
} from "fs";
|
|
23353
24754
|
import { isIP } from "net";
|
|
23354
|
-
import { join as
|
|
24755
|
+
import { join as join14 } from "path";
|
|
23355
24756
|
var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
23356
24757
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
23357
24758
|
var FETCH_TIMEOUT_MS = 30000;
|
|
23358
24759
|
var MAX_REDIRECTS = 5;
|
|
23359
24760
|
function cacheDir(storageDir) {
|
|
23360
|
-
return
|
|
24761
|
+
return join14(storageDir, "url_cache");
|
|
23361
24762
|
}
|
|
23362
24763
|
function hashUrl(url2) {
|
|
23363
|
-
return
|
|
24764
|
+
return createHash4("sha256").update(url2).digest("hex").slice(0, 16);
|
|
23364
24765
|
}
|
|
23365
24766
|
function metaPath(storageDir, hash2) {
|
|
23366
|
-
return
|
|
24767
|
+
return join14(cacheDir(storageDir), `${hash2}.meta.json`);
|
|
23367
24768
|
}
|
|
23368
24769
|
function contentPath(storageDir, hash2, extension) {
|
|
23369
|
-
return
|
|
24770
|
+
return join14(cacheDir(storageDir), `${hash2}${extension}`);
|
|
23370
24771
|
}
|
|
23371
24772
|
function _isPrivateIpv4(address) {
|
|
23372
24773
|
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
@@ -23525,15 +24926,15 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
|
|
|
23525
24926
|
}
|
|
23526
24927
|
const allowPrivate = options.allowPrivate === true;
|
|
23527
24928
|
const dir = cacheDir(storageDir);
|
|
23528
|
-
|
|
24929
|
+
mkdirSync9(dir, { recursive: true });
|
|
23529
24930
|
const hash2 = hashUrl(url2);
|
|
23530
24931
|
const metaFile = metaPath(storageDir, hash2);
|
|
23531
|
-
if (
|
|
24932
|
+
if (existsSync10(metaFile)) {
|
|
23532
24933
|
try {
|
|
23533
|
-
const meta4 = JSON.parse(
|
|
24934
|
+
const meta4 = JSON.parse(readFileSync5(metaFile, "utf8"));
|
|
23534
24935
|
const age = Date.now() - meta4.fetchedAt;
|
|
23535
24936
|
const cached2 = contentPath(storageDir, hash2, meta4.extension);
|
|
23536
|
-
if (age < CACHE_TTL_MS &&
|
|
24937
|
+
if (age < CACHE_TTL_MS && existsSync10(cached2)) {
|
|
23537
24938
|
log(`URL cache hit: ${url2} (${Math.round(age / 1000)}s old)`);
|
|
23538
24939
|
return cached2;
|
|
23539
24940
|
}
|
|
@@ -23578,9 +24979,9 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
|
|
|
23578
24979
|
const body = Buffer.concat(chunks);
|
|
23579
24980
|
const contentFile = contentPath(storageDir, hash2, extension);
|
|
23580
24981
|
const tmpContent = `${contentFile}.tmp-${process.pid}`;
|
|
23581
|
-
|
|
23582
|
-
const { renameSync:
|
|
23583
|
-
|
|
24982
|
+
writeFileSync5(tmpContent, body);
|
|
24983
|
+
const { renameSync: renameSync4 } = await import("fs");
|
|
24984
|
+
renameSync4(tmpContent, contentFile);
|
|
23584
24985
|
const meta3 = {
|
|
23585
24986
|
url: url2,
|
|
23586
24987
|
contentType,
|
|
@@ -23588,35 +24989,35 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
|
|
|
23588
24989
|
fetchedAt: Date.now()
|
|
23589
24990
|
};
|
|
23590
24991
|
const tmpMeta = `${metaFile}.tmp-${process.pid}`;
|
|
23591
|
-
|
|
23592
|
-
|
|
24992
|
+
writeFileSync5(tmpMeta, JSON.stringify(meta3));
|
|
24993
|
+
renameSync4(tmpMeta, metaFile);
|
|
23593
24994
|
log(`URL cached (${total} bytes): ${url2}`);
|
|
23594
24995
|
return contentFile;
|
|
23595
24996
|
}
|
|
23596
24997
|
function cleanupUrlCache(storageDir) {
|
|
23597
24998
|
const dir = cacheDir(storageDir);
|
|
23598
|
-
if (!
|
|
24999
|
+
if (!existsSync10(dir))
|
|
23599
25000
|
return;
|
|
23600
25001
|
let removed = 0;
|
|
23601
25002
|
try {
|
|
23602
|
-
for (const entry of
|
|
25003
|
+
for (const entry of readdirSync4(dir)) {
|
|
23603
25004
|
if (!entry.endsWith(".meta.json"))
|
|
23604
25005
|
continue;
|
|
23605
|
-
const metaFile =
|
|
25006
|
+
const metaFile = join14(dir, entry);
|
|
23606
25007
|
try {
|
|
23607
|
-
const meta3 = JSON.parse(
|
|
25008
|
+
const meta3 = JSON.parse(readFileSync5(metaFile, "utf8"));
|
|
23608
25009
|
const age = Date.now() - meta3.fetchedAt;
|
|
23609
25010
|
if (age > CACHE_TTL_MS) {
|
|
23610
25011
|
const hash2 = entry.slice(0, -".meta.json".length);
|
|
23611
25012
|
const content = contentPath(storageDir, hash2, meta3.extension);
|
|
23612
|
-
if (
|
|
23613
|
-
|
|
23614
|
-
|
|
25013
|
+
if (existsSync10(content))
|
|
25014
|
+
unlinkSync6(content);
|
|
25015
|
+
unlinkSync6(metaFile);
|
|
23615
25016
|
removed++;
|
|
23616
25017
|
}
|
|
23617
25018
|
} catch {
|
|
23618
25019
|
try {
|
|
23619
|
-
|
|
25020
|
+
unlinkSync6(metaFile);
|
|
23620
25021
|
removed++;
|
|
23621
25022
|
} catch {}
|
|
23622
25023
|
}
|
|
@@ -25375,7 +26776,7 @@ function navigationTools(ctx) {
|
|
|
25375
26776
|
|
|
25376
26777
|
// src/tools/reading.ts
|
|
25377
26778
|
import { readdir } from "fs/promises";
|
|
25378
|
-
import { extname as extname2, join as
|
|
26779
|
+
import { extname as extname2, join as join15, resolve as resolve6 } from "path";
|
|
25379
26780
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
25380
26781
|
var OUTLINE_EXTENSIONS = new Set([
|
|
25381
26782
|
".ts",
|
|
@@ -25453,7 +26854,7 @@ function readingTools(ctx) {
|
|
|
25453
26854
|
if (!dirArg && typeof args.filePath === "string" && !Array.isArray(args.files)) {
|
|
25454
26855
|
try {
|
|
25455
26856
|
const { stat } = await import("fs/promises");
|
|
25456
|
-
const resolved =
|
|
26857
|
+
const resolved = resolve6(context.directory, args.filePath);
|
|
25457
26858
|
const st = await stat(resolved);
|
|
25458
26859
|
if (st.isDirectory()) {
|
|
25459
26860
|
dirArg = args.filePath;
|
|
@@ -25461,7 +26862,7 @@ function readingTools(ctx) {
|
|
|
25461
26862
|
} catch {}
|
|
25462
26863
|
}
|
|
25463
26864
|
if (dirArg) {
|
|
25464
|
-
const dirPath =
|
|
26865
|
+
const dirPath = resolve6(context.directory, dirArg);
|
|
25465
26866
|
const files = await discoverSourceFiles(dirPath);
|
|
25466
26867
|
if (files.length === 0) {
|
|
25467
26868
|
return JSON.stringify({
|
|
@@ -25569,12 +26970,12 @@ async function discoverSourceFiles(dir, maxFiles = 200) {
|
|
|
25569
26970
|
return;
|
|
25570
26971
|
if (entry.isDirectory()) {
|
|
25571
26972
|
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
25572
|
-
await walk(
|
|
26973
|
+
await walk(join15(current, entry.name));
|
|
25573
26974
|
}
|
|
25574
26975
|
} else if (entry.isFile()) {
|
|
25575
26976
|
const ext = extname2(entry.name).toLowerCase();
|
|
25576
26977
|
if (OUTLINE_EXTENSIONS.has(ext)) {
|
|
25577
|
-
files.push(
|
|
26978
|
+
files.push(join15(current, entry.name));
|
|
25578
26979
|
}
|
|
25579
26980
|
}
|
|
25580
26981
|
}
|
|
@@ -26167,8 +27568,8 @@ var plugin = async (input) => {
|
|
|
26167
27568
|
if (aftConfig.max_callgraph_files !== undefined)
|
|
26168
27569
|
configOverrides.max_callgraph_files = aftConfig.max_callgraph_files;
|
|
26169
27570
|
const isFastembedSemanticBackend = (aftConfig.semantic?.backend ?? "fastembed") === "fastembed";
|
|
26170
|
-
const dataHome = process.env.XDG_DATA_HOME ||
|
|
26171
|
-
configOverrides.storage_dir =
|
|
27571
|
+
const dataHome = process.env.XDG_DATA_HOME || join16(homedir8(), ".local", "share");
|
|
27572
|
+
configOverrides.storage_dir = join16(dataHome, "opencode", "storage", "plugin", "aft");
|
|
26172
27573
|
if (aftConfig.experimental_semantic_search && isFastembedSemanticBackend) {
|
|
26173
27574
|
const storageDir2 = configOverrides.storage_dir;
|
|
26174
27575
|
const ortDylibDir = await ensureOnnxRuntime(storageDir2).catch((err) => {
|
|
@@ -26181,6 +27582,63 @@ var plugin = async (input) => {
|
|
|
26181
27582
|
warn(`Semantic search requires ONNX Runtime. Install: ${getManualInstallHint()}`);
|
|
26182
27583
|
}
|
|
26183
27584
|
}
|
|
27585
|
+
try {
|
|
27586
|
+
const lspAutoInstall = aftConfig.lsp?.auto_install ?? true;
|
|
27587
|
+
const lspGraceDays = aftConfig.lsp?.grace_days ?? 7;
|
|
27588
|
+
const lspVersions = aftConfig.lsp?.versions ?? {};
|
|
27589
|
+
const lspDisabled = new Set(aftConfig.lsp?.disabled ?? []);
|
|
27590
|
+
const npmResult = runAutoInstall(input.directory, {
|
|
27591
|
+
autoInstall: lspAutoInstall,
|
|
27592
|
+
graceDays: lspGraceDays,
|
|
27593
|
+
versions: lspVersions,
|
|
27594
|
+
disabled: lspDisabled
|
|
27595
|
+
});
|
|
27596
|
+
const relevantGithub = discoverRelevantGithubServers(input.directory);
|
|
27597
|
+
const ghResult = runGithubAutoInstall(relevantGithub, {
|
|
27598
|
+
autoInstall: lspAutoInstall,
|
|
27599
|
+
graceDays: lspGraceDays,
|
|
27600
|
+
versions: lspVersions,
|
|
27601
|
+
disabled: lspDisabled
|
|
27602
|
+
});
|
|
27603
|
+
const mergedBinDirs = [...npmResult.cachedBinDirs, ...ghResult.cachedBinDirs];
|
|
27604
|
+
if (mergedBinDirs.length > 0) {
|
|
27605
|
+
configOverrides.lsp_paths_extra = mergedBinDirs;
|
|
27606
|
+
}
|
|
27607
|
+
if (npmResult.installsStarted > 0 || ghResult.installsStarted > 0) {
|
|
27608
|
+
log(`[lsp] auto-install: ${npmResult.installsStarted} npm + ${ghResult.installsStarted} github install(s) running in background`);
|
|
27609
|
+
}
|
|
27610
|
+
Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
|
|
27611
|
+
const actionable = [...npmResult.skipped, ...ghResult.skipped].filter((s) => {
|
|
27612
|
+
const r = s.reason.toLowerCase();
|
|
27613
|
+
if (r === "auto_install: false")
|
|
27614
|
+
return false;
|
|
27615
|
+
if (r === "disabled by config")
|
|
27616
|
+
return false;
|
|
27617
|
+
if (r === "not relevant to project")
|
|
27618
|
+
return false;
|
|
27619
|
+
if (r === "already installed")
|
|
27620
|
+
return false;
|
|
27621
|
+
if (r === "another install in progress")
|
|
27622
|
+
return false;
|
|
27623
|
+
return true;
|
|
27624
|
+
});
|
|
27625
|
+
if (actionable.length === 0)
|
|
27626
|
+
return;
|
|
27627
|
+
const lines = actionable.map((s) => ` \u2022 ${s.id}: ${s.reason}`).join(`
|
|
27628
|
+
`);
|
|
27629
|
+
const message = `AFT skipped or failed to install ${actionable.length} LSP server(s):
|
|
27630
|
+
${lines}
|
|
27631
|
+
|
|
27632
|
+
` + "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.";
|
|
27633
|
+
sendWarning({ client: input.client, directory: input.directory }, message).catch((err) => {
|
|
27634
|
+
warn(`[lsp] failed to deliver install summary: ${err}`);
|
|
27635
|
+
});
|
|
27636
|
+
}).catch((err) => {
|
|
27637
|
+
warn(`[lsp] install-summary aggregation failed: ${err}`);
|
|
27638
|
+
});
|
|
27639
|
+
} catch (err) {
|
|
27640
|
+
warn(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
27641
|
+
}
|
|
26184
27642
|
let versionUpgradeAttempted = null;
|
|
26185
27643
|
const pool = new BridgePool(binaryPath, {
|
|
26186
27644
|
minVersion: PLUGIN_VERSION,
|
|
@@ -26228,6 +27686,7 @@ var plugin = async (input) => {
|
|
|
26228
27686
|
setSharedBridgePool(pool);
|
|
26229
27687
|
const rpcServer = new AftRpcServer(configOverrides.storage_dir, input.directory);
|
|
26230
27688
|
const unregisterShutdown = registerShutdownCleanup(async () => {
|
|
27689
|
+
await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
|
|
26231
27690
|
try {
|
|
26232
27691
|
rpcServer.stop();
|
|
26233
27692
|
} catch {}
|
|
@@ -26244,10 +27703,10 @@ var plugin = async (input) => {
|
|
|
26244
27703
|
return { show: false };
|
|
26245
27704
|
}
|
|
26246
27705
|
if (storageDir) {
|
|
26247
|
-
const versionFile =
|
|
27706
|
+
const versionFile = join16(storageDir, "last_announced_version");
|
|
26248
27707
|
try {
|
|
26249
|
-
if (
|
|
26250
|
-
const lastVersion =
|
|
27708
|
+
if (existsSync12(versionFile)) {
|
|
27709
|
+
const lastVersion = readFileSync6(versionFile, "utf-8").trim();
|
|
26251
27710
|
if (lastVersion === ANNOUNCEMENT_VERSION)
|
|
26252
27711
|
return { show: false };
|
|
26253
27712
|
}
|
|
@@ -26258,8 +27717,8 @@ var plugin = async (input) => {
|
|
|
26258
27717
|
rpcServer.handle("mark-announced", async () => {
|
|
26259
27718
|
if (storageDir && ANNOUNCEMENT_VERSION) {
|
|
26260
27719
|
try {
|
|
26261
|
-
|
|
26262
|
-
|
|
27720
|
+
mkdirSync10(storageDir, { recursive: true });
|
|
27721
|
+
writeFileSync6(join16(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
|
|
26263
27722
|
} catch {}
|
|
26264
27723
|
}
|
|
26265
27724
|
return { success: true };
|
|
@@ -26389,11 +27848,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
26389
27848
|
}
|
|
26390
27849
|
};
|
|
26391
27850
|
},
|
|
26392
|
-
dispose: () => {
|
|
27851
|
+
dispose: async () => {
|
|
26393
27852
|
unregisterShutdown();
|
|
27853
|
+
await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
|
|
26394
27854
|
rpcServer.stop();
|
|
26395
27855
|
clearSharedBridgePool();
|
|
26396
|
-
|
|
27856
|
+
await pool.shutdown();
|
|
26397
27857
|
}
|
|
26398
27858
|
};
|
|
26399
27859
|
};
|