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