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