nanos-lint 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/action.yml +1 -1
- package/dist/{cli-sEO_MZ-K.js → cli-DGez8MSI.js} +643 -203
- package/dist/cli-DGez8MSI.js.map +1 -0
- package/dist/cli-r8LDolDk.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +108 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/templates/.luarc.json +15 -21
- package/dist/cli-sEO_MZ-K.js.map +0 -1
|
@@ -929,6 +929,77 @@ function cleanCache(targetDir = systemPaths.cache) {
|
|
|
929
929
|
}
|
|
930
930
|
return null;
|
|
931
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Recursively calculates total disk usage of a file or directory in bytes.
|
|
934
|
+
* Handles missing paths and inaccessible files gracefully.
|
|
935
|
+
*/
|
|
936
|
+
function getDirectorySize(targetPath) {
|
|
937
|
+
if (!fs.existsSync(targetPath)) return 0;
|
|
938
|
+
let total = 0;
|
|
939
|
+
try {
|
|
940
|
+
const stat = fs.statSync(targetPath);
|
|
941
|
+
if (!stat.isDirectory()) return stat.size;
|
|
942
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
943
|
+
for (const entry of entries) {
|
|
944
|
+
const fullPath = `${targetPath}/${entry.name}`;
|
|
945
|
+
try {
|
|
946
|
+
if (entry.isDirectory()) total += getDirectorySize(fullPath);
|
|
947
|
+
else if (entry.isFile()) total += fs.statSync(fullPath).size;
|
|
948
|
+
} catch (err) {
|
|
949
|
+
logger.debug(`[paths] Failed to stat entry ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch (err) {
|
|
953
|
+
logger.debug(`[paths] Failed to calculate directory size for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
954
|
+
return 0;
|
|
955
|
+
}
|
|
956
|
+
return total;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Formats byte values into human-readable strings (e.g. "86.4 MB", "1.42 MB", "42.5 MB").
|
|
960
|
+
*/
|
|
961
|
+
function formatBytes(bytes) {
|
|
962
|
+
if (bytes <= 0 || !Number.isFinite(bytes)) return "0 B";
|
|
963
|
+
if (bytes < 1) return `${Math.round(bytes)} B`;
|
|
964
|
+
const units = [
|
|
965
|
+
"B",
|
|
966
|
+
"KB",
|
|
967
|
+
"MB",
|
|
968
|
+
"GB",
|
|
969
|
+
"TB"
|
|
970
|
+
];
|
|
971
|
+
let i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
|
|
972
|
+
let val = bytes / Math.pow(1024, i);
|
|
973
|
+
if (Math.round(val) >= 1024 && i < units.length - 1) {
|
|
974
|
+
i++;
|
|
975
|
+
val /= 1024;
|
|
976
|
+
}
|
|
977
|
+
return `${i === 0 ? val.toFixed(0) : val < 10 ? val.toFixed(2) : val < 100 ? val.toFixed(1) : val.toFixed(0)} ${units[i]}`;
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/errors.ts
|
|
981
|
+
var NanosLintError = class extends Error {
|
|
982
|
+
code;
|
|
983
|
+
remedy;
|
|
984
|
+
name = "NanosLintError";
|
|
985
|
+
constructor(message, code, remedy, options) {
|
|
986
|
+
super(message, options);
|
|
987
|
+
this.code = code;
|
|
988
|
+
this.remedy = remedy;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
var ConfigError = class extends NanosLintError {
|
|
992
|
+
name = "ConfigError";
|
|
993
|
+
};
|
|
994
|
+
var LuaLSError = class extends NanosLintError {
|
|
995
|
+
name = "LuaLSError";
|
|
996
|
+
};
|
|
997
|
+
var AnnotationsError = class extends NanosLintError {
|
|
998
|
+
name = "AnnotationsError";
|
|
999
|
+
};
|
|
1000
|
+
var CacheError = class extends NanosLintError {
|
|
1001
|
+
name = "CacheError";
|
|
1002
|
+
};
|
|
932
1003
|
//#endregion
|
|
933
1004
|
//#region src/config.ts
|
|
934
1005
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -980,8 +1051,14 @@ function parseJsonc(text) {
|
|
|
980
1051
|
return result;
|
|
981
1052
|
}
|
|
982
1053
|
function loadConfigFile(filePath) {
|
|
983
|
-
if (!fs.existsSync(filePath)) throw new
|
|
984
|
-
|
|
1054
|
+
if (!fs.existsSync(filePath)) throw new ConfigError(`Configuration file not found: ${filePath}`, "ERR_CONFIG_NOT_FOUND", "Ensure the configuration file path is correct or run 'nanos-lint init' to generate a default config.");
|
|
1055
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1056
|
+
try {
|
|
1057
|
+
return parseJsonc(content);
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
if (err instanceof ConfigError) throw err;
|
|
1060
|
+
throw new ConfigError(`Failed to parse configuration file at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, "ERR_CONFIG_PARSE", "Check your .luarc.json syntax or run 'nanos-lint init --force' to scaffold a clean template.", { cause: err });
|
|
1061
|
+
}
|
|
985
1062
|
}
|
|
986
1063
|
/**
|
|
987
1064
|
* Removes trailing `/` characters from a path-like string.
|
|
@@ -997,6 +1074,76 @@ function stripTrailingSlashes(value) {
|
|
|
997
1074
|
return end === value.length ? value : value.slice(0, end);
|
|
998
1075
|
}
|
|
999
1076
|
/**
|
|
1077
|
+
* Set of recognized diagnostic codes supported by Lua Language Server (LuaLS).
|
|
1078
|
+
* LuaLS discards the entire `diagnostics.severity` object if it contains a single
|
|
1079
|
+
* unrecognized key (such as obsolete 'syntax-error'). nanos-lint filters severity
|
|
1080
|
+
* and neededFileStatus maps against this allow-list to ensure valid configuration.
|
|
1081
|
+
*/
|
|
1082
|
+
const VALID_LUALS_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
1083
|
+
"ambiguity-1",
|
|
1084
|
+
"assign-type-mismatch",
|
|
1085
|
+
"await-in-sync",
|
|
1086
|
+
"cast-local-type",
|
|
1087
|
+
"cast-type-mismatch",
|
|
1088
|
+
"circle-doc-class",
|
|
1089
|
+
"close-non-object",
|
|
1090
|
+
"code-after-break",
|
|
1091
|
+
"codestyle-check",
|
|
1092
|
+
"count-down-loop",
|
|
1093
|
+
"deprecated",
|
|
1094
|
+
"different-requires",
|
|
1095
|
+
"discard-returns",
|
|
1096
|
+
"doc-field-no-class",
|
|
1097
|
+
"duplicate-doc-alias",
|
|
1098
|
+
"duplicate-doc-field",
|
|
1099
|
+
"duplicate-doc-param",
|
|
1100
|
+
"duplicate-index",
|
|
1101
|
+
"duplicate-set-field",
|
|
1102
|
+
"empty-block",
|
|
1103
|
+
"global-element",
|
|
1104
|
+
"global-in-nil-env",
|
|
1105
|
+
"incomplete-signature-doc",
|
|
1106
|
+
"inject-field",
|
|
1107
|
+
"invisible",
|
|
1108
|
+
"lowercase-global",
|
|
1109
|
+
"missing-fields",
|
|
1110
|
+
"missing-global-doc",
|
|
1111
|
+
"missing-local-export-doc",
|
|
1112
|
+
"missing-parameter",
|
|
1113
|
+
"missing-return",
|
|
1114
|
+
"missing-return-value",
|
|
1115
|
+
"name-style-check",
|
|
1116
|
+
"need-check-nil",
|
|
1117
|
+
"newfield-call",
|
|
1118
|
+
"newline-call",
|
|
1119
|
+
"no-unknown",
|
|
1120
|
+
"not-yieldable",
|
|
1121
|
+
"param-type-mismatch",
|
|
1122
|
+
"redefined-local",
|
|
1123
|
+
"redundant-parameter",
|
|
1124
|
+
"redundant-return",
|
|
1125
|
+
"redundant-return-value",
|
|
1126
|
+
"redundant-value",
|
|
1127
|
+
"return-type-mismatch",
|
|
1128
|
+
"spell-check",
|
|
1129
|
+
"trailing-space",
|
|
1130
|
+
"unbalanced-assignments",
|
|
1131
|
+
"undefined-doc-class",
|
|
1132
|
+
"undefined-doc-name",
|
|
1133
|
+
"undefined-doc-param",
|
|
1134
|
+
"undefined-env-child",
|
|
1135
|
+
"undefined-field",
|
|
1136
|
+
"undefined-global",
|
|
1137
|
+
"unknown-cast-variable",
|
|
1138
|
+
"unknown-diag-code",
|
|
1139
|
+
"unknown-operator",
|
|
1140
|
+
"unreachable-code",
|
|
1141
|
+
"unused-function",
|
|
1142
|
+
"unused-label",
|
|
1143
|
+
"unused-local",
|
|
1144
|
+
"unused-vararg"
|
|
1145
|
+
]);
|
|
1146
|
+
/**
|
|
1000
1147
|
* Merges a base nanos configuration with a workspace override configuration.
|
|
1001
1148
|
* Guarantees that nanos API annotations are included in workspace.library,
|
|
1002
1149
|
* and standardizes paths for LuaLS.
|
|
@@ -1027,10 +1174,20 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
|
|
|
1027
1174
|
const baseGlobals = base.diagnostics?.globals ?? [];
|
|
1028
1175
|
const overrideGlobals = override.diagnostics?.globals ?? [];
|
|
1029
1176
|
const globalsSet = /* @__PURE__ */ new Set([...baseGlobals, ...overrideGlobals]);
|
|
1030
|
-
const
|
|
1177
|
+
const rawSeverity = {
|
|
1031
1178
|
...base.diagnostics?.severity ?? {},
|
|
1032
1179
|
...override.diagnostics?.severity ?? {}
|
|
1033
1180
|
};
|
|
1181
|
+
const mergedSeverity = {};
|
|
1182
|
+
for (const [code, level] of Object.entries(rawSeverity)) if (VALID_LUALS_DIAGNOSTIC_CODES.has(code)) mergedSeverity[code] = level;
|
|
1183
|
+
else logger.warn(`[config] Unrecognized diagnostic code "${code}" in diagnostics.severity was dropped to prevent LuaLS from discarding the severity configuration.`);
|
|
1184
|
+
const rawNeededFileStatus = {
|
|
1185
|
+
...base.diagnostics?.neededFileStatus ?? {},
|
|
1186
|
+
...override.diagnostics?.neededFileStatus ?? {}
|
|
1187
|
+
};
|
|
1188
|
+
const mergedNeededFileStatus = {};
|
|
1189
|
+
for (const [code, status] of Object.entries(rawNeededFileStatus)) if (VALID_LUALS_DIAGNOSTIC_CODES.has(code)) mergedNeededFileStatus[code] = status;
|
|
1190
|
+
else logger.warn(`[config] Unrecognized diagnostic code "${code}" in diagnostics.neededFileStatus was dropped.`);
|
|
1034
1191
|
const hasCliIgnore = Boolean(options?.cliIgnore && options.cliIgnore.length > 0);
|
|
1035
1192
|
let mergedIgnoreDir;
|
|
1036
1193
|
let mergedFilesExclude;
|
|
@@ -1103,7 +1260,8 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
|
|
|
1103
1260
|
...base.diagnostics ?? {},
|
|
1104
1261
|
...override.diagnostics ?? {},
|
|
1105
1262
|
globals: Array.from(globalsSet),
|
|
1106
|
-
severity: mergedSeverity
|
|
1263
|
+
severity: mergedSeverity,
|
|
1264
|
+
...Object.keys(mergedNeededFileStatus).length > 0 ? { neededFileStatus: mergedNeededFileStatus } : {}
|
|
1107
1265
|
}
|
|
1108
1266
|
};
|
|
1109
1267
|
}
|
|
@@ -1165,10 +1323,10 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
|
|
|
1165
1323
|
*/
|
|
1166
1324
|
function initWorkspace(workspacePath, options) {
|
|
1167
1325
|
const targetFile = path.join(workspacePath, ".luarc.json");
|
|
1168
|
-
if (fs.existsSync(targetFile) && !options?.force) throw new
|
|
1326
|
+
if (fs.existsSync(targetFile) && !options?.force) throw new ConfigError(`.luarc.json already exists at ${targetFile}. Use --force to overwrite.`, "ERR_CONFIG_EXISTS", "Pass --force to overwrite the existing .luarc.json file.");
|
|
1169
1327
|
const template = loadConfigFile(getDefaultTemplatePath());
|
|
1170
1328
|
const sourceAnnotations = options?.annotationsPath || getDefaultAnnotationsPath();
|
|
1171
|
-
if (!fs.existsSync(sourceAnnotations)) throw new
|
|
1329
|
+
if (!fs.existsSync(sourceAnnotations)) throw new ConfigError(`Definitions file not found at ${sourceAnnotations}. Make sure annotations have been downloaded or pass a valid file with --annotations.`, "ERR_ANNOTATIONS_NOT_FOUND", "Run 'nanos-lint warmup' to download annotations or provide --annotations <path>.");
|
|
1172
1330
|
const targetNanosDir = path.join(workspacePath, ".nanos-lint");
|
|
1173
1331
|
fs.mkdirSync(targetNanosDir, { recursive: true });
|
|
1174
1332
|
const targetAnnotations = path.join(targetNanosDir, "annotations.lua");
|
|
@@ -1184,8 +1342,7 @@ function initWorkspace(workspacePath, options) {
|
|
|
1184
1342
|
return targetFile;
|
|
1185
1343
|
}
|
|
1186
1344
|
//#endregion
|
|
1187
|
-
//#region src/luals.ts
|
|
1188
|
-
const execFileAsync = promisify(execFile);
|
|
1345
|
+
//#region src/luals/version.ts
|
|
1189
1346
|
const FALLBACK_LUALS_VERSION = "3.19.1";
|
|
1190
1347
|
const DEFAULT_LUALS_VERSION = "latest";
|
|
1191
1348
|
/**
|
|
@@ -1222,12 +1379,6 @@ function sanitizeLuaLSVersion(raw) {
|
|
|
1222
1379
|
return first >= 48 && first <= 57 || first >= 65 && first <= 90 || first >= 97 && first <= 122 ? version : null;
|
|
1223
1380
|
}
|
|
1224
1381
|
/**
|
|
1225
|
-
* Escapes single quotes for safe PowerShell single-quoted string interpolation.
|
|
1226
|
-
*/
|
|
1227
|
-
function escapePowerShellSingleQuote(str) {
|
|
1228
|
-
return str.replace(/'/g, "''");
|
|
1229
|
-
}
|
|
1230
|
-
/**
|
|
1231
1382
|
* Fetches the latest available LuaLS release tag from the GitHub API.
|
|
1232
1383
|
* Returns null if the request fails, times out, or receives an invalid tag.
|
|
1233
1384
|
*/
|
|
@@ -1264,9 +1415,11 @@ async function resolveLatestLuaLSVersion() {
|
|
|
1264
1415
|
async function resolveLuaLSVersion(version) {
|
|
1265
1416
|
if (!version || version === "latest") return await resolveLatestLuaLSVersion();
|
|
1266
1417
|
const sanitized = sanitizeLuaLSVersion(version);
|
|
1267
|
-
if (!sanitized) throw new
|
|
1418
|
+
if (!sanitized) throw new LuaLSError(`Invalid LuaLS version: "${version}". Expected a release tag such as "3.19.1", or "latest".`, "ERR_LUALS_INVALID_VERSION", "Provide a valid release tag like '3.19.1' or use 'latest'.");
|
|
1268
1419
|
return sanitized;
|
|
1269
1420
|
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/luals/platform.ts
|
|
1270
1423
|
function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
1271
1424
|
const platform = process.platform;
|
|
1272
1425
|
const arch = process.arch;
|
|
@@ -1277,7 +1430,7 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
|
1277
1430
|
assetName: `lua-language-server-${version}-win32-x64.zip`,
|
|
1278
1431
|
binaryRelativePath: path.join("bin", "lua-language-server.exe")
|
|
1279
1432
|
};
|
|
1280
|
-
throw new
|
|
1433
|
+
throw new LuaLSError(`Unsupported Windows architecture: ${arch}. Supported: x64`, "ERR_LUALS_UNSUPPORTED_ARCH", "LuaLS provides pre-built binaries for Windows x64. Use a compatible system or set LUALS_BIN to a custom binary.");
|
|
1281
1434
|
}
|
|
1282
1435
|
if (platform === "linux") {
|
|
1283
1436
|
if (arch === "x64") return {
|
|
@@ -1292,7 +1445,7 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
|
1292
1445
|
assetName: `lua-language-server-${version}-linux-arm64.tar.gz`,
|
|
1293
1446
|
binaryRelativePath: path.join("bin", "lua-language-server")
|
|
1294
1447
|
};
|
|
1295
|
-
throw new
|
|
1448
|
+
throw new LuaLSError(`Unsupported Linux architecture: ${arch}. Supported: x64, arm64`, "ERR_LUALS_UNSUPPORTED_ARCH", "LuaLS provides pre-built binaries for Linux x64 and arm64. Use LUALS_BIN to specify a custom build.");
|
|
1296
1449
|
}
|
|
1297
1450
|
if (platform === "darwin") return {
|
|
1298
1451
|
platform: "darwin",
|
|
@@ -1300,8 +1453,31 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
|
1300
1453
|
assetName: `lua-language-server-${version}-darwin-${arch === "arm64" ? "arm64" : "x64"}.tar.gz`,
|
|
1301
1454
|
binaryRelativePath: path.join("bin", "lua-language-server")
|
|
1302
1455
|
};
|
|
1303
|
-
throw new
|
|
1456
|
+
throw new LuaLSError(`Unsupported platform: ${platform}`, "ERR_LUALS_UNSUPPORTED_PLATFORM", "LuaLS provides pre-built binaries for Windows, Linux, and macOS. Use LUALS_BIN to specify a custom build.");
|
|
1457
|
+
}
|
|
1458
|
+
//#endregion
|
|
1459
|
+
//#region src/luals/validation.ts
|
|
1460
|
+
/**
|
|
1461
|
+
* Verifies that a LuaLS binary exists, has non-trivial size, and is executable.
|
|
1462
|
+
*/
|
|
1463
|
+
function isBinaryValid(binaryPath) {
|
|
1464
|
+
if (!fs.existsSync(binaryPath)) return false;
|
|
1465
|
+
try {
|
|
1466
|
+
const stats = fs.statSync(binaryPath);
|
|
1467
|
+
if (!stats.isFile() || stats.size < 1e5) return false;
|
|
1468
|
+
const output = execFileSync(binaryPath, ["--version"], {
|
|
1469
|
+
timeout: 5e3,
|
|
1470
|
+
stdio: "pipe",
|
|
1471
|
+
encoding: "utf-8"
|
|
1472
|
+
});
|
|
1473
|
+
return /^\d+\.\d+\.\d+/.test(output.trim());
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1304
1478
|
}
|
|
1479
|
+
//#endregion
|
|
1480
|
+
//#region src/luals/cache.ts
|
|
1305
1481
|
function getBaseLuaLSCacheDir() {
|
|
1306
1482
|
return path.join(systemPaths.cache, "luals");
|
|
1307
1483
|
}
|
|
@@ -1341,8 +1517,20 @@ function readLuaLSMetadata(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
|
1341
1517
|
const content = fs.readFileSync(metaPath, "utf-8");
|
|
1342
1518
|
const parsed = JSON.parse(content);
|
|
1343
1519
|
if (typeof parsed?.lastCheckedWeek === "string" && typeof parsed?.latestVersion === "string") return parsed;
|
|
1520
|
+
try {
|
|
1521
|
+
fs.unlinkSync(metaPath);
|
|
1522
|
+
logger.warn(`[luals] Stale or invalid LuaLS metadata at ${metaPath} purged.`);
|
|
1523
|
+
} catch (unlinkErr) {
|
|
1524
|
+
logger.debug(`[luals] Failed to unlink invalid metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
1525
|
+
}
|
|
1344
1526
|
} catch (err) {
|
|
1345
1527
|
logger.debug(`[luals] Failed to parse LuaLS metadata: ${err instanceof Error ? err.message : String(err)}`);
|
|
1528
|
+
try {
|
|
1529
|
+
fs.unlinkSync(metaPath);
|
|
1530
|
+
logger.warn(`[luals] Corrupted LuaLS metadata at ${metaPath} purged.`);
|
|
1531
|
+
} catch (unlinkErr) {
|
|
1532
|
+
logger.debug(`[luals] Failed to unlink corrupted metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
1533
|
+
}
|
|
1346
1534
|
}
|
|
1347
1535
|
return null;
|
|
1348
1536
|
}
|
|
@@ -1356,6 +1544,26 @@ function writeLuaLSMetadata(metadata, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
|
1356
1544
|
}
|
|
1357
1545
|
}
|
|
1358
1546
|
/**
|
|
1547
|
+
* Lists all candidate LuaLS version directories under baseCacheDir without running
|
|
1548
|
+
* execution smoke tests on the binaries.
|
|
1549
|
+
*/
|
|
1550
|
+
function listCachedLuaLSVersionDirs(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1551
|
+
if (!fs.existsSync(baseCacheDir)) return [];
|
|
1552
|
+
try {
|
|
1553
|
+
const entries = fs.readdirSync(baseCacheDir, { withFileTypes: true });
|
|
1554
|
+
const versions = [];
|
|
1555
|
+
for (const entry of entries) {
|
|
1556
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1557
|
+
const version = sanitizeLuaLSVersion(entry.name);
|
|
1558
|
+
if (version) versions.push(version);
|
|
1559
|
+
}
|
|
1560
|
+
return versions;
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
logger.debug(`[luals] Failed to list cached LuaLS version directories: ${err instanceof Error ? err.message : String(err)}`);
|
|
1563
|
+
return [];
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1359
1567
|
* Lists all cached LuaLS versions in baseCacheDir that have a valid .complete marker
|
|
1360
1568
|
* and a functional executable binary.
|
|
1361
1569
|
*/
|
|
@@ -1432,36 +1640,28 @@ function findExistingLuaLSDir(version, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
|
1432
1640
|
logger.debug(`[luals] Error checking primary LuaLS cache marker at ${primaryMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1433
1641
|
}
|
|
1434
1642
|
const legacyCache = getLegacyCacheDir(version);
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (fs.
|
|
1439
|
-
|
|
1440
|
-
|
|
1643
|
+
if (path.resolve(legacyCache) !== path.resolve(primaryCache)) {
|
|
1644
|
+
const legacyBin = path.join(legacyCache, info.binaryRelativePath);
|
|
1645
|
+
const legacyMarker = path.join(legacyCache, ".complete");
|
|
1646
|
+
if (fs.existsSync(legacyMarker)) try {
|
|
1647
|
+
if (fs.readFileSync(legacyMarker, "utf-8").trim() === version && isBinaryValid(legacyBin)) return legacyCache;
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
logger.debug(`[luals] Error checking legacy LuaLS cache marker at ${legacyMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1650
|
+
}
|
|
1441
1651
|
}
|
|
1442
1652
|
const pkgRoot = getPackageRoot();
|
|
1443
1653
|
const pkgBin = path.join(pkgRoot, info.binaryRelativePath);
|
|
1444
1654
|
if (fs.existsSync(pkgBin) && isBinaryValid(pkgBin)) return pkgRoot;
|
|
1445
1655
|
return null;
|
|
1446
1656
|
}
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/luals/download.ts
|
|
1659
|
+
const execFileAsync$1 = promisify(execFile);
|
|
1447
1660
|
/**
|
|
1448
|
-
*
|
|
1661
|
+
* Escapes single quotes for safe PowerShell single-quoted string interpolation.
|
|
1449
1662
|
*/
|
|
1450
|
-
function
|
|
1451
|
-
|
|
1452
|
-
try {
|
|
1453
|
-
const stats = fs.statSync(binaryPath);
|
|
1454
|
-
if (!stats.isFile() || stats.size < 1e5) return false;
|
|
1455
|
-
const output = execFileSync(binaryPath, ["--version"], {
|
|
1456
|
-
timeout: 5e3,
|
|
1457
|
-
stdio: "pipe",
|
|
1458
|
-
encoding: "utf-8"
|
|
1459
|
-
});
|
|
1460
|
-
return /^\d+\.\d+\.\d+/.test(output.trim());
|
|
1461
|
-
} catch (err) {
|
|
1462
|
-
logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1463
|
-
return false;
|
|
1464
|
-
}
|
|
1663
|
+
function escapePowerShellSingleQuote(str) {
|
|
1664
|
+
return str.replace(/'/g, "''");
|
|
1465
1665
|
}
|
|
1466
1666
|
async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDir, options) {
|
|
1467
1667
|
const resolvedVersion = await resolveLuaLSVersion(version);
|
|
@@ -1470,7 +1670,7 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1470
1670
|
const binaryPath = path.join(destDir, info.binaryRelativePath);
|
|
1471
1671
|
const completeMarker = path.join(destDir, ".complete");
|
|
1472
1672
|
if (fs.existsSync(destDir)) {
|
|
1473
|
-
if (fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
|
|
1673
|
+
if (options?.reuseExisting !== false && fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
|
|
1474
1674
|
if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(binaryPath)) return binaryPath;
|
|
1475
1675
|
} catch (err) {
|
|
1476
1676
|
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1514,19 +1714,22 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1514
1714
|
}
|
|
1515
1715
|
if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
|
|
1516
1716
|
}
|
|
1517
|
-
if (!response || !response.body)
|
|
1717
|
+
if (!response || !response.body) {
|
|
1718
|
+
if (lastErr instanceof LuaLSError) throw lastErr;
|
|
1719
|
+
throw new LuaLSError(`Failed to download LuaLS from ${url}${lastErr ? `: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}` : ""}`, "ERR_LUALS_DOWNLOAD", "Check your network connection or specify a custom binary with LUALS_BIN.", { cause: lastErr });
|
|
1720
|
+
}
|
|
1518
1721
|
const arrayBuffer = await response.arrayBuffer();
|
|
1519
1722
|
fs.writeFileSync(archivePath, Buffer.from(arrayBuffer));
|
|
1520
1723
|
if (!options?.quiet) logger.info(`[luals] Extracting to ${destDir}...`);
|
|
1521
1724
|
try {
|
|
1522
|
-
await execFileAsync("tar", [
|
|
1725
|
+
await execFileAsync$1("tar", [
|
|
1523
1726
|
"-xf",
|
|
1524
1727
|
archivePath,
|
|
1525
1728
|
"-C",
|
|
1526
1729
|
tempDir
|
|
1527
1730
|
]);
|
|
1528
1731
|
} catch (tarErr) {
|
|
1529
|
-
if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync("powershell.exe", [
|
|
1732
|
+
if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync$1("powershell.exe", [
|
|
1530
1733
|
"-NoProfile",
|
|
1531
1734
|
"-Command",
|
|
1532
1735
|
`Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
|
|
@@ -1545,8 +1748,8 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1545
1748
|
} catch (err) {
|
|
1546
1749
|
logger.warn(`[luals] Failed to chmod binary at ${tempBinaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1547
1750
|
}
|
|
1548
|
-
if (!fs.existsSync(tempBinaryPath) || fs.statSync(tempBinaryPath).size < 1e5) throw new
|
|
1549
|
-
if (!isBinaryValid(tempBinaryPath)) throw new
|
|
1751
|
+
if (!fs.existsSync(tempBinaryPath) || fs.statSync(tempBinaryPath).size < 1e5) throw new LuaLSError(`Failed to extract valid LuaLS binary to expected path: ${tempBinaryPath}`, "ERR_LUALS_EXTRACT", "Run 'nanos-lint clean-cache' and ensure there is sufficient disk space.");
|
|
1752
|
+
if (!isBinaryValid(tempBinaryPath)) throw new LuaLSError(`Extracted LuaLS binary at ${tempBinaryPath} is invalid or non-functional.`, "ERR_LUALS_INVALID_BINARY", "Run 'nanos-lint clean-cache' to remove corrupted downloads or set LUALS_BIN to a custom binary.");
|
|
1550
1753
|
fs.writeFileSync(path.join(tempDir, ".complete"), resolvedVersion, "utf-8");
|
|
1551
1754
|
for (let attempt = 0; attempt < 5; attempt++) try {
|
|
1552
1755
|
fs.renameSync(tempDir, destDir);
|
|
@@ -1590,9 +1793,111 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1590
1793
|
}
|
|
1591
1794
|
}
|
|
1592
1795
|
}
|
|
1796
|
+
//#endregion
|
|
1797
|
+
//#region src/luals/files.ts
|
|
1798
|
+
/**
|
|
1799
|
+
* Counts candidate Lua files within targetPath, taking ignoreDir and files.exclude into account.
|
|
1800
|
+
*/
|
|
1801
|
+
function countCheckedFiles(targetPath, configPath) {
|
|
1802
|
+
const absPath = path.resolve(targetPath);
|
|
1803
|
+
if (!fs.existsSync(absPath)) return 0;
|
|
1804
|
+
if (fs.statSync(absPath).isFile()) return absPath.toLowerCase().endsWith(".lua") ? 1 : 0;
|
|
1805
|
+
let ignoreDirs = [
|
|
1806
|
+
".git",
|
|
1807
|
+
".vscode",
|
|
1808
|
+
".nanos-lint",
|
|
1809
|
+
"node_modules"
|
|
1810
|
+
];
|
|
1811
|
+
let excludePatterns = [];
|
|
1812
|
+
if (configPath && fs.existsSync(configPath)) try {
|
|
1813
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1814
|
+
if (cfg.workspace?.ignoreDir) ignoreDirs = cfg.workspace.ignoreDir;
|
|
1815
|
+
if (cfg.files?.exclude) excludePatterns = cfg.files.exclude;
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
logger.warn(`[luals] Failed to parse config file for file counting at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1818
|
+
}
|
|
1819
|
+
const normIgnoreDirs = new Set(ignoreDirs.map((d) => d.replace(/\\/g, "/").toLowerCase()));
|
|
1820
|
+
function isExcluded(relPath) {
|
|
1821
|
+
const norm = relPath.replace(/\\/g, "/");
|
|
1822
|
+
const baseName = path.posix.basename(norm);
|
|
1823
|
+
for (const pat of excludePatterns) {
|
|
1824
|
+
const normPat = pat.replace(/\\/g, "/");
|
|
1825
|
+
if (norm === normPat || baseName === normPat) return true;
|
|
1826
|
+
if (normPat.endsWith("/**")) {
|
|
1827
|
+
const dir = normPat.slice(0, -3);
|
|
1828
|
+
if (norm === dir || norm.startsWith(`${dir}/`)) return true;
|
|
1829
|
+
}
|
|
1830
|
+
if (norm.startsWith(`${normPat}/`)) return true;
|
|
1831
|
+
if (normPat.includes("*") || normPat.includes("?")) {
|
|
1832
|
+
if (!normPat.includes("/")) {
|
|
1833
|
+
const baseRegexStr = "^" + normPat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
1834
|
+
try {
|
|
1835
|
+
if (new RegExp(baseRegexStr, "i").test(baseName)) return true;
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
logger.debug(`[luals] Invalid regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
let regexStr = normPat;
|
|
1841
|
+
const hasLeadingDoubleStar = regexStr.startsWith("**/");
|
|
1842
|
+
if (hasLeadingDoubleStar) regexStr = regexStr.slice(3);
|
|
1843
|
+
const hasTrailingDoubleStar = regexStr.endsWith("/**");
|
|
1844
|
+
if (hasTrailingDoubleStar) regexStr = regexStr.slice(0, -3);
|
|
1845
|
+
let escaped = regexStr.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "/(?:.*/)?").replace(/\*\*/g, ".*").replace(/(?<!\.)\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
1846
|
+
if (hasLeadingDoubleStar) escaped = `(?:^|.*/)${escaped}`;
|
|
1847
|
+
if (hasTrailingDoubleStar) escaped = `${escaped}(?:/.*)?`;
|
|
1848
|
+
try {
|
|
1849
|
+
if (new RegExp(`^${escaped}$`, "i").test(norm)) return true;
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
logger.debug(`[luals] Invalid glob regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
let count = 0;
|
|
1858
|
+
function walk(currentDir, relDir = "") {
|
|
1859
|
+
let entries;
|
|
1860
|
+
try {
|
|
1861
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
logger.debug(`[luals] Failed to read directory ${currentDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
for (const entry of entries) {
|
|
1867
|
+
const name = entry.name;
|
|
1868
|
+
const relPath = relDir ? `${relDir}/${name}` : name;
|
|
1869
|
+
const fullPath = path.join(currentDir, name);
|
|
1870
|
+
if (entry.isDirectory() || entry.isSymbolicLink() && (() => {
|
|
1871
|
+
try {
|
|
1872
|
+
return fs.statSync(fullPath).isDirectory();
|
|
1873
|
+
} catch (err) {
|
|
1874
|
+
logger.debug(`[luals] Failed to stat symlink target ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
})()) {
|
|
1878
|
+
const lowerName = name.toLowerCase();
|
|
1879
|
+
if (normIgnoreDirs.has(lowerName) || normIgnoreDirs.has(relPath.toLowerCase())) continue;
|
|
1880
|
+
if (isExcluded(relPath) || isExcluded(`${relPath}/**`)) continue;
|
|
1881
|
+
walk(fullPath, relPath);
|
|
1882
|
+
} else if (entry.isFile() && name.toLowerCase().endsWith(".lua")) {
|
|
1883
|
+
if (!isExcluded(relPath)) count++;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
walk(absPath);
|
|
1888
|
+
return count;
|
|
1889
|
+
}
|
|
1890
|
+
//#endregion
|
|
1891
|
+
//#region src/luals/runner.ts
|
|
1892
|
+
const execFileAsync = promisify(execFile);
|
|
1893
|
+
function isOfflineError(err) {
|
|
1894
|
+
if (!err) return false;
|
|
1895
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1896
|
+
return /offline|enotfound|eai_again|econnrefused|etimedout|fetch failed/i.test(msg);
|
|
1897
|
+
}
|
|
1593
1898
|
async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
1594
1899
|
if (process.env.LUALS_BIN && fs.existsSync(process.env.LUALS_BIN)) return process.env.LUALS_BIN;
|
|
1595
|
-
if (!version || version === "latest") {
|
|
1900
|
+
if (options?.reuseExisting !== false && (!version || version === "latest")) {
|
|
1596
1901
|
const defaultInfo = getPlatformInfo(FALLBACK_LUALS_VERSION);
|
|
1597
1902
|
const defaultBundledPath = path.join(getPackageRoot(), defaultInfo.binaryRelativePath);
|
|
1598
1903
|
if (fs.existsSync(defaultBundledPath) && isBinaryValid(defaultBundledPath)) return defaultBundledPath;
|
|
@@ -1602,18 +1907,28 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
1602
1907
|
if (!isDefaultOrLatest) {
|
|
1603
1908
|
const resolvedVersion = await resolveLuaLSVersion(version);
|
|
1604
1909
|
const info = getPlatformInfo(resolvedVersion);
|
|
1605
|
-
|
|
1606
|
-
|
|
1910
|
+
if (options?.reuseExisting !== false) {
|
|
1911
|
+
const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
|
|
1912
|
+
if (fs.existsSync(bundledPath) && isBinaryValid(bundledPath)) return bundledPath;
|
|
1913
|
+
}
|
|
1607
1914
|
const cachedDir = getCacheDir(resolvedVersion, baseCacheDir);
|
|
1608
1915
|
const cachedPath = path.join(cachedDir, info.binaryRelativePath);
|
|
1609
1916
|
const completeMarker = path.join(cachedDir, ".complete");
|
|
1917
|
+
let wasCorrupted = false;
|
|
1610
1918
|
if (fs.existsSync(cachedPath)) {
|
|
1919
|
+
let isValid = false;
|
|
1611
1920
|
if (fs.existsSync(completeMarker)) try {
|
|
1612
|
-
if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath))
|
|
1921
|
+
if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath)) {
|
|
1922
|
+
isValid = true;
|
|
1923
|
+
if (options?.reuseExisting !== false) return cachedPath;
|
|
1924
|
+
}
|
|
1613
1925
|
} catch (err) {
|
|
1614
1926
|
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1615
1927
|
}
|
|
1616
|
-
if (!
|
|
1928
|
+
if (!isValid) {
|
|
1929
|
+
wasCorrupted = true;
|
|
1930
|
+
if (!options?.quiet) logger.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
|
|
1931
|
+
}
|
|
1617
1932
|
try {
|
|
1618
1933
|
fs.rmSync(cachedDir, {
|
|
1619
1934
|
recursive: true,
|
|
@@ -1623,29 +1938,33 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
1623
1938
|
logger.warn(`[luals] Failed to remove corrupted cache directory ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1624
1939
|
}
|
|
1625
1940
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1941
|
+
if (options?.reuseExisting !== false) {
|
|
1942
|
+
const legacyDir = getLegacyCacheDir(resolvedVersion);
|
|
1943
|
+
if (path.resolve(legacyDir) !== path.resolve(cachedDir)) {
|
|
1944
|
+
const legacyPath = path.join(legacyDir, info.binaryRelativePath);
|
|
1945
|
+
const legacyMarker = path.join(legacyDir, ".complete");
|
|
1946
|
+
if (fs.existsSync(legacyPath)) {
|
|
1947
|
+
let validLegacy = false;
|
|
1948
|
+
if (fs.existsSync(legacyMarker)) try {
|
|
1949
|
+
if (fs.readFileSync(legacyMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(legacyPath)) validLegacy = true;
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
logger.debug(`[luals] Failed to read legacy complete marker at ${legacyMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1952
|
+
}
|
|
1953
|
+
else if (isBinaryValid(legacyPath)) validLegacy = true;
|
|
1954
|
+
if (validLegacy) {
|
|
1955
|
+
try {
|
|
1956
|
+
fs.mkdirSync(path.dirname(cachedDir), { recursive: true });
|
|
1957
|
+
fs.cpSync(legacyDir, cachedDir, { recursive: true });
|
|
1958
|
+
if (fs.existsSync(cachedPath) && isBinaryValid(cachedPath)) return cachedPath;
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
logger.warn(`[luals] Failed to migrate legacy cache from ${legacyDir} to ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1961
|
+
}
|
|
1962
|
+
return legacyPath;
|
|
1963
|
+
}
|
|
1644
1964
|
}
|
|
1645
|
-
return legacyPath;
|
|
1646
1965
|
}
|
|
1647
1966
|
}
|
|
1648
|
-
try {
|
|
1967
|
+
if (options?.reuseExisting !== false) try {
|
|
1649
1968
|
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
1650
1969
|
const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
|
|
1651
1970
|
const found = stdout.trim().split(/\r?\n/)[0];
|
|
@@ -1653,20 +1972,29 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
1653
1972
|
} catch (err) {
|
|
1654
1973
|
logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
|
|
1655
1974
|
}
|
|
1656
|
-
|
|
1975
|
+
try {
|
|
1976
|
+
return await downloadAndExtractLuaLS(resolvedVersion, getCacheDir(resolvedVersion, baseCacheDir), options);
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
if (wasCorrupted) {
|
|
1979
|
+
const isOffline = isOfflineError(err);
|
|
1980
|
+
const reason = isOffline ? "while offline" : err instanceof Error ? err.message : String(err);
|
|
1981
|
+
throw new LuaLSError(`Cached LuaLS binary at '${cachedPath}' is corrupted (failed execution/size check) and cannot be re-downloaded ${isOffline ? reason : `: ${reason}`}. Please ${isOffline ? "connect to the internet" : "verify your network connection"} to repair or run 'nanos-lint clean-cache'.`, "ERR_LUALS_CORRUPTED_CACHE", "Connect to the internet to repair the corrupted binary or run 'nanos-lint clean-cache'.", { cause: err });
|
|
1982
|
+
}
|
|
1983
|
+
throw err;
|
|
1984
|
+
}
|
|
1657
1985
|
}
|
|
1658
1986
|
const currentWeek = getIsoWeek();
|
|
1659
1987
|
const metadata = readLuaLSMetadata(baseCacheDir);
|
|
1660
|
-
if (metadata && metadata.lastCheckedWeek === currentWeek && metadata.latestVersion) {
|
|
1988
|
+
if (options?.reuseExisting !== false && metadata && metadata.lastCheckedWeek === currentWeek && metadata.latestVersion) {
|
|
1661
1989
|
const info = getPlatformInfo(metadata.latestVersion);
|
|
1662
1990
|
const cachedPath = path.join(getCacheDir(metadata.latestVersion, baseCacheDir), info.binaryRelativePath);
|
|
1663
1991
|
if (isBinaryValid(cachedPath)) return cachedPath;
|
|
1664
1992
|
}
|
|
1665
|
-
const cachedVersions = listCachedLuaLSVersions(baseCacheDir);
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const info = getPlatformInfo(
|
|
1669
|
-
return path.join(getCacheDir(
|
|
1993
|
+
const cachedVersions = options?.reuseExisting !== false ? listCachedLuaLSVersions(baseCacheDir) : [];
|
|
1994
|
+
const firstCachedVersion = cachedVersions[0];
|
|
1995
|
+
if (options?.reuseExisting !== false && metadata && metadata.lastCheckedWeek === currentWeek && firstCachedVersion) {
|
|
1996
|
+
const info = getPlatformInfo(firstCachedVersion);
|
|
1997
|
+
return path.join(getCacheDir(firstCachedVersion, baseCacheDir), info.binaryRelativePath);
|
|
1670
1998
|
}
|
|
1671
1999
|
const onlineTag = await fetchLatestLuaLSVersionFromGitHub();
|
|
1672
2000
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -1675,9 +2003,9 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
1675
2003
|
else if (metadata?.latestVersion && cachedVersions.includes(metadata.latestVersion)) {
|
|
1676
2004
|
logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${metadata.latestVersion}.`);
|
|
1677
2005
|
targetVersion = metadata.latestVersion;
|
|
1678
|
-
} else if (
|
|
1679
|
-
logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${
|
|
1680
|
-
targetVersion =
|
|
2006
|
+
} else if (firstCachedVersion) {
|
|
2007
|
+
logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${firstCachedVersion}.`);
|
|
2008
|
+
targetVersion = firstCachedVersion;
|
|
1681
2009
|
} else targetVersion = FALLBACK_LUALS_VERSION;
|
|
1682
2010
|
writeLuaLSMetadata({
|
|
1683
2011
|
lastCheckedWeek: currentWeek,
|
|
@@ -1688,11 +2016,16 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
1688
2016
|
const targetCacheDir = getCacheDir(targetVersion, baseCacheDir);
|
|
1689
2017
|
const targetBinaryPath = path.join(targetCacheDir, info.binaryRelativePath);
|
|
1690
2018
|
const completeMarker = path.join(targetCacheDir, ".complete");
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2019
|
+
let wasCorrupted = false;
|
|
2020
|
+
if (fs.existsSync(targetBinaryPath)) {
|
|
2021
|
+
if (fs.existsSync(completeMarker) && isBinaryValid(targetBinaryPath)) {
|
|
2022
|
+
if (options?.reuseExisting !== false) {
|
|
2023
|
+
cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
|
|
2024
|
+
return targetBinaryPath;
|
|
2025
|
+
}
|
|
2026
|
+
} else wasCorrupted = true;
|
|
1694
2027
|
}
|
|
1695
|
-
if (!onlineTag) try {
|
|
2028
|
+
if (options?.reuseExisting !== false && !onlineTag) try {
|
|
1696
2029
|
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
1697
2030
|
const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
|
|
1698
2031
|
const found = stdout.trim().split(/\r?\n/)[0];
|
|
@@ -1700,13 +2033,28 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
1700
2033
|
} catch (err) {
|
|
1701
2034
|
logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
|
|
1702
2035
|
}
|
|
1703
|
-
|
|
2036
|
+
let downloadedBinary;
|
|
2037
|
+
try {
|
|
2038
|
+
downloadedBinary = await downloadAndExtractLuaLS(targetVersion, targetCacheDir, options);
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
if (wasCorrupted) {
|
|
2041
|
+
const isOffline = isOfflineError(err);
|
|
2042
|
+
const reason = isOffline ? "while offline" : err instanceof Error ? err.message : String(err);
|
|
2043
|
+
throw new LuaLSError(`Cached LuaLS binary at '${targetBinaryPath}' is corrupted (failed execution/size check) and cannot be re-downloaded ${isOffline ? reason : `: ${reason}`}. Please ${isOffline ? "connect to the internet" : "verify your network connection"} to repair or run 'nanos-lint clean-cache'.`, "ERR_LUALS_CORRUPTED_CACHE", "Connect to the internet to repair the corrupted binary or run 'nanos-lint clean-cache'.", { cause: err });
|
|
2044
|
+
}
|
|
2045
|
+
throw err;
|
|
2046
|
+
}
|
|
1704
2047
|
cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
|
|
1705
2048
|
return downloadedBinary;
|
|
1706
2049
|
}
|
|
1707
2050
|
async function runLuaLSCheck(targetPath, configPath, options) {
|
|
1708
|
-
|
|
1709
|
-
|
|
2051
|
+
let absoluteTarget = path.resolve(targetPath);
|
|
2052
|
+
try {
|
|
2053
|
+
if (fs.existsSync(absoluteTarget)) absoluteTarget = fs.realpathSync.native(absoluteTarget);
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
logger.debug(`[luals] Failed to resolve native realpath for target: ${err instanceof Error ? err.message : String(err)}`);
|
|
2056
|
+
}
|
|
2057
|
+
if (!fs.existsSync(absoluteTarget)) throw new LuaLSError(`Target path does not exist: ${targetPath}`, "ERR_TARGET_NOT_FOUND", "Verify that the target path exists and is accessible.");
|
|
1710
2058
|
const binary = options.lualsBin || await resolveLuaLSBinary(options.lualsVersion, { quiet: options.quiet });
|
|
1711
2059
|
let checkDir = absoluteTarget;
|
|
1712
2060
|
let targetFileOnly = null;
|
|
@@ -1751,16 +2099,21 @@ async function runLuaLSCheck(targetPath, configPath, options) {
|
|
|
1751
2099
|
}
|
|
1752
2100
|
if (!parseSucceeded) {
|
|
1753
2101
|
const cacheHint = `(Cache location: ${getCacheDir()})`;
|
|
1754
|
-
if (execError) throw new
|
|
1755
|
-
throw new
|
|
2102
|
+
if (execError) throw new LuaLSError(`LuaLS check failed to execute or produce diagnostic output: ${execError instanceof Error ? execError.message : String(execError)}. ${cacheHint}`, "ERR_LUALS_EXECUTION", "Inspect the debug log with --log-level=debug or run 'nanos-lint clean-cache' to re-fetch LuaLS.", { cause: execError });
|
|
2103
|
+
throw new LuaLSError(`LuaLS check failed to produce diagnostic output at: ${checkOutPath}. ${cacheHint}`, "ERR_LUALS_NO_OUTPUT", "Ensure the temporary directory is writable and sufficient disk space is available.");
|
|
1756
2104
|
}
|
|
1757
2105
|
if (targetFileOnly) {
|
|
1758
2106
|
const filtered = {};
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2107
|
+
const normalizeComparable = (p) => {
|
|
2108
|
+
try {
|
|
2109
|
+
if (fs.existsSync(p)) return fs.realpathSync.native(p).replace(/\\/g, "/").toLowerCase();
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
logger.debug(`[luals] Failed to resolve native realpath for comparison: ${err instanceof Error ? err.message : String(err)}`);
|
|
2112
|
+
}
|
|
2113
|
+
return path.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
2114
|
+
};
|
|
2115
|
+
const normTarget = normalizeComparable(targetFileOnly);
|
|
2116
|
+
for (const [rawUri, diags] of Object.entries(diagnostics)) if (normalizeComparable(fileUriToPath(rawUri)) === normTarget) filtered[rawUri] = diags;
|
|
1764
2117
|
diagnostics = filtered;
|
|
1765
2118
|
}
|
|
1766
2119
|
let totalProblems = 0;
|
|
@@ -1785,98 +2138,6 @@ async function runLuaLSCheck(targetPath, configPath, options) {
|
|
|
1785
2138
|
diagnostics
|
|
1786
2139
|
};
|
|
1787
2140
|
}
|
|
1788
|
-
/**
|
|
1789
|
-
* Counts candidate Lua files within targetPath, taking ignoreDir and files.exclude into account.
|
|
1790
|
-
*/
|
|
1791
|
-
function countCheckedFiles(targetPath, configPath) {
|
|
1792
|
-
const absPath = path.resolve(targetPath);
|
|
1793
|
-
if (!fs.existsSync(absPath)) return 0;
|
|
1794
|
-
if (fs.statSync(absPath).isFile()) return absPath.toLowerCase().endsWith(".lua") ? 1 : 0;
|
|
1795
|
-
let ignoreDirs = [
|
|
1796
|
-
".git",
|
|
1797
|
-
".vscode",
|
|
1798
|
-
".nanos-lint",
|
|
1799
|
-
"node_modules"
|
|
1800
|
-
];
|
|
1801
|
-
let excludePatterns = [];
|
|
1802
|
-
if (configPath && fs.existsSync(configPath)) try {
|
|
1803
|
-
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1804
|
-
if (cfg.workspace?.ignoreDir) ignoreDirs = cfg.workspace.ignoreDir;
|
|
1805
|
-
if (cfg.files?.exclude) excludePatterns = cfg.files.exclude;
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
logger.warn(`[luals] Failed to parse config file for file counting at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1808
|
-
}
|
|
1809
|
-
const normIgnoreDirs = new Set(ignoreDirs.map((d) => d.replace(/\\/g, "/").toLowerCase()));
|
|
1810
|
-
function isExcluded(relPath) {
|
|
1811
|
-
const norm = relPath.replace(/\\/g, "/");
|
|
1812
|
-
const baseName = path.posix.basename(norm);
|
|
1813
|
-
for (const pat of excludePatterns) {
|
|
1814
|
-
const normPat = pat.replace(/\\/g, "/");
|
|
1815
|
-
if (norm === normPat || baseName === normPat) return true;
|
|
1816
|
-
if (normPat.endsWith("/**")) {
|
|
1817
|
-
const dir = normPat.slice(0, -3);
|
|
1818
|
-
if (norm === dir || norm.startsWith(`${dir}/`)) return true;
|
|
1819
|
-
}
|
|
1820
|
-
if (norm.startsWith(`${normPat}/`)) return true;
|
|
1821
|
-
if (normPat.includes("*") || normPat.includes("?")) {
|
|
1822
|
-
if (!normPat.includes("/")) {
|
|
1823
|
-
const baseRegexStr = "^" + normPat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
1824
|
-
try {
|
|
1825
|
-
if (new RegExp(baseRegexStr, "i").test(baseName)) return true;
|
|
1826
|
-
} catch (err) {
|
|
1827
|
-
logger.debug(`[luals] Invalid regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
let regexStr = normPat;
|
|
1831
|
-
const hasLeadingDoubleStar = regexStr.startsWith("**/");
|
|
1832
|
-
if (hasLeadingDoubleStar) regexStr = regexStr.slice(3);
|
|
1833
|
-
const hasTrailingDoubleStar = regexStr.endsWith("/**");
|
|
1834
|
-
if (hasTrailingDoubleStar) regexStr = regexStr.slice(0, -3);
|
|
1835
|
-
let escaped = regexStr.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "/(?:.*/)?").replace(/\*\*/g, ".*").replace(/(?<!\.)\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
1836
|
-
if (hasLeadingDoubleStar) escaped = `(?:^|.*/)${escaped}`;
|
|
1837
|
-
if (hasTrailingDoubleStar) escaped = `${escaped}(?:/.*)?`;
|
|
1838
|
-
try {
|
|
1839
|
-
if (new RegExp(`^${escaped}$`, "i").test(norm)) return true;
|
|
1840
|
-
} catch (err) {
|
|
1841
|
-
logger.debug(`[luals] Invalid glob regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
return false;
|
|
1846
|
-
}
|
|
1847
|
-
let count = 0;
|
|
1848
|
-
function walk(currentDir, relDir = "") {
|
|
1849
|
-
let entries;
|
|
1850
|
-
try {
|
|
1851
|
-
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1852
|
-
} catch (err) {
|
|
1853
|
-
logger.debug(`[luals] Failed to read directory ${currentDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1854
|
-
return;
|
|
1855
|
-
}
|
|
1856
|
-
for (const entry of entries) {
|
|
1857
|
-
const name = entry.name;
|
|
1858
|
-
const relPath = relDir ? `${relDir}/${name}` : name;
|
|
1859
|
-
const fullPath = path.join(currentDir, name);
|
|
1860
|
-
if (entry.isDirectory() || entry.isSymbolicLink() && (() => {
|
|
1861
|
-
try {
|
|
1862
|
-
return fs.statSync(fullPath).isDirectory();
|
|
1863
|
-
} catch (err) {
|
|
1864
|
-
logger.debug(`[luals] Failed to stat symlink target ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1865
|
-
return false;
|
|
1866
|
-
}
|
|
1867
|
-
})()) {
|
|
1868
|
-
const lowerName = name.toLowerCase();
|
|
1869
|
-
if (normIgnoreDirs.has(lowerName) || normIgnoreDirs.has(relPath.toLowerCase())) continue;
|
|
1870
|
-
if (isExcluded(relPath) || isExcluded(`${relPath}/**`)) continue;
|
|
1871
|
-
walk(fullPath, relPath);
|
|
1872
|
-
} else if (entry.isFile() && name.toLowerCase().endsWith(".lua")) {
|
|
1873
|
-
if (!isExcluded(relPath)) count++;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
walk(absPath);
|
|
1878
|
-
return count;
|
|
1879
|
-
}
|
|
1880
2141
|
//#endregion
|
|
1881
2142
|
//#region src/reporter.ts
|
|
1882
2143
|
const SEVERITY_NAMES = {
|
|
@@ -5013,11 +5274,43 @@ function readAnnotationsMetadata(cacheDir = getAnnotationsCacheDir()) {
|
|
|
5013
5274
|
const content = fs.readFileSync(metaPath, "utf-8");
|
|
5014
5275
|
const parsed = JSON.parse(content);
|
|
5015
5276
|
if (typeof parsed?.commitId === "string" && typeof parsed?.lastChecked === "string") return parsed;
|
|
5277
|
+
try {
|
|
5278
|
+
fs.unlinkSync(metaPath);
|
|
5279
|
+
logger.warn(`[annotations] Stale or invalid annotations metadata at ${metaPath} purged.`);
|
|
5280
|
+
} catch (unlinkErr) {
|
|
5281
|
+
logger.debug(`[annotations] Failed to unlink invalid metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
5282
|
+
}
|
|
5016
5283
|
} catch (err) {
|
|
5017
5284
|
logger.debug(`Failed to parse annotations metadata: ${err instanceof Error ? err.message : String(err)}`);
|
|
5285
|
+
try {
|
|
5286
|
+
fs.unlinkSync(metaPath);
|
|
5287
|
+
logger.warn(`[annotations] Corrupted annotations metadata at ${metaPath} purged.`);
|
|
5288
|
+
} catch (unlinkErr) {
|
|
5289
|
+
logger.debug(`[annotations] Failed to unlink corrupted metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
5290
|
+
}
|
|
5018
5291
|
}
|
|
5019
5292
|
return null;
|
|
5020
5293
|
}
|
|
5294
|
+
/**
|
|
5295
|
+
* Verifies that an annotations file exists, is a regular file, is of non-trivial size (>= 1000 bytes),
|
|
5296
|
+
* and begins with valid Lua comments or nanos world definitions.
|
|
5297
|
+
*/
|
|
5298
|
+
function isAnnotationsValid(filePath) {
|
|
5299
|
+
if (!fs.existsSync(filePath)) return false;
|
|
5300
|
+
try {
|
|
5301
|
+
const stat = fs.statSync(filePath);
|
|
5302
|
+
if (!stat.isFile() || stat.size < 1e3) return false;
|
|
5303
|
+
const fd = fs.openSync(filePath, "r");
|
|
5304
|
+
const buffer = Buffer.alloc(256);
|
|
5305
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 256, 0);
|
|
5306
|
+
fs.closeSync(fd);
|
|
5307
|
+
const header = buffer.toString("utf-8", 0, bytesRead).trimStart();
|
|
5308
|
+
return header.startsWith("--") || header.includes("nanos world");
|
|
5309
|
+
} catch (err) {
|
|
5310
|
+
logger.debug(`[annotations] Annotation validation failed for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5311
|
+
return false;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5021
5314
|
async function fetchLatestCommitId() {
|
|
5022
5315
|
try {
|
|
5023
5316
|
const headers = { "User-Agent": "nanos-lint" };
|
|
@@ -5040,9 +5333,9 @@ async function fetchRawAnnotationsContent() {
|
|
|
5040
5333
|
headers: { "User-Agent": "nanos-lint" },
|
|
5041
5334
|
signal: AbortSignal.timeout(15e3)
|
|
5042
5335
|
});
|
|
5043
|
-
if (!res.ok) throw new
|
|
5336
|
+
if (!res.ok) throw new AnnotationsError(`Failed to download annotations.lua: ${res.status} ${res.statusText}`, "ERR_ANNOTATIONS_DOWNLOAD", "Verify your internet connection and that GitHub raw endpoints are accessible.");
|
|
5044
5337
|
const text = await res.text();
|
|
5045
|
-
if (!text || text.length < 1e3) throw new
|
|
5338
|
+
if (!text || text.length < 1e3) throw new AnnotationsError("Downloaded annotations.lua appears truncated or invalid", "ERR_ANNOTATIONS_INVALID", "Retry downloading or pass a local annotations file via --annotations <path>.");
|
|
5046
5339
|
return text;
|
|
5047
5340
|
}
|
|
5048
5341
|
async function copyFileWithRetry(src, dest, maxRetries = 10) {
|
|
@@ -5164,31 +5457,39 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
|
|
|
5164
5457
|
async function resolveAnnotations(options = {}) {
|
|
5165
5458
|
if (options.customPath) {
|
|
5166
5459
|
const resolved = path.resolve(options.customPath);
|
|
5167
|
-
if (!fs.existsSync(resolved)) throw new
|
|
5460
|
+
if (!fs.existsSync(resolved)) throw new AnnotationsError(`Custom annotations file not found: ${resolved}`, "ERR_ANNOTATIONS_NOT_FOUND", "Verify that the path specified in --annotations exists and is accessible.");
|
|
5168
5461
|
return resolved;
|
|
5169
5462
|
}
|
|
5170
5463
|
const envPath = process.env.NANOS_ANNOTATIONS_PATH || process.env.NANOS_ANNOTATIONS;
|
|
5171
5464
|
if (envPath) {
|
|
5172
5465
|
const resolved = path.resolve(envPath);
|
|
5173
|
-
if (!fs.existsSync(resolved)) throw new
|
|
5466
|
+
if (!fs.existsSync(resolved)) throw new AnnotationsError(`Annotations file specified in environment not found: ${resolved}`, "ERR_ANNOTATIONS_ENV_NOT_FOUND", "Verify that the path specified in NANOS_ANNOTATIONS_PATH or NANOS_ANNOTATIONS exists.");
|
|
5174
5467
|
return resolved;
|
|
5175
5468
|
}
|
|
5176
5469
|
const bundled = path.join(getPackageRoot(), "annotations.lua");
|
|
5177
|
-
if (fs.existsSync(bundled)) return bundled;
|
|
5470
|
+
if (fs.existsSync(bundled) && isAnnotationsValid(bundled)) return bundled;
|
|
5178
5471
|
const cacheDir = options.cacheDir ?? getAnnotationsCacheDir();
|
|
5179
5472
|
const cachedAnnotationsFile = path.join(cacheDir, ANNOTATIONS_FILENAME);
|
|
5180
5473
|
const metadata = readAnnotationsMetadata(cacheDir);
|
|
5181
5474
|
const { dateStr } = getTodayDateString();
|
|
5182
|
-
if (
|
|
5475
|
+
if (fs.existsSync(cachedAnnotationsFile) && !isAnnotationsValid(cachedAnnotationsFile)) {
|
|
5476
|
+
logger.warn("[annotations] Cached annotations.lua is corrupted or empty. Purging and refreshing...");
|
|
5477
|
+
try {
|
|
5478
|
+
fs.unlinkSync(cachedAnnotationsFile);
|
|
5479
|
+
} catch (err) {
|
|
5480
|
+
logger.warn(`[annotations] Failed to remove corrupted cached annotations: ${err instanceof Error ? err.message : String(err)}`);
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
if (metadata && metadata.lastChecked === dateStr && fs.existsSync(cachedAnnotationsFile) && isAnnotationsValid(cachedAnnotationsFile)) return cachedAnnotationsFile;
|
|
5183
5484
|
const latestCommitId = await fetchLatestCommitId();
|
|
5184
5485
|
if (latestCommitId) {
|
|
5185
|
-
if (metadata && metadata.commitId === latestCommitId && fs.existsSync(cachedAnnotationsFile)) {
|
|
5486
|
+
if (metadata && metadata.commitId === latestCommitId && fs.existsSync(cachedAnnotationsFile) && isAnnotationsValid(cachedAnnotationsFile)) {
|
|
5186
5487
|
updateLastCheckedDate(latestCommitId, cacheDir);
|
|
5187
5488
|
return cachedAnnotationsFile;
|
|
5188
5489
|
}
|
|
5189
5490
|
return await downloadAndCacheAnnotations(latestCommitId, cacheDir, options);
|
|
5190
5491
|
}
|
|
5191
|
-
if (fs.existsSync(cachedAnnotationsFile)) {
|
|
5492
|
+
if (fs.existsSync(cachedAnnotationsFile) && isAnnotationsValid(cachedAnnotationsFile)) {
|
|
5192
5493
|
try {
|
|
5193
5494
|
updateLastCheckedDate(metadata?.commitId || "unknown", cacheDir);
|
|
5194
5495
|
} catch (err) {
|
|
@@ -5199,6 +5500,10 @@ async function resolveAnnotations(options = {}) {
|
|
|
5199
5500
|
try {
|
|
5200
5501
|
return await downloadAndCacheAnnotations("unknown", cacheDir, options);
|
|
5201
5502
|
} catch (err) {
|
|
5503
|
+
if (fs.existsSync(bundled) && isAnnotationsValid(bundled)) {
|
|
5504
|
+
logger.info("[annotations] Network offline and cache unavailable. Falling back to bundled annotations.");
|
|
5505
|
+
return bundled;
|
|
5506
|
+
}
|
|
5202
5507
|
if (Boolean(err && typeof err === "object" && "code" in err && typeof err.code === "string" && [
|
|
5203
5508
|
"EACCES",
|
|
5204
5509
|
"EPERM",
|
|
@@ -5206,9 +5511,100 @@ async function resolveAnnotations(options = {}) {
|
|
|
5206
5511
|
"EROFS",
|
|
5207
5512
|
"EEXIST",
|
|
5208
5513
|
"ENOENT"
|
|
5209
|
-
].includes(err.code)) || err instanceof Error && /permission denied|read-only|no space left/i.test(err.message)) throw new
|
|
5210
|
-
throw new
|
|
5514
|
+
].includes(err.code)) || err instanceof Error && /permission denied|read-only|no space left/i.test(err.message)) throw new AnnotationsError(`Failed to resolve nanos world API annotations due to a filesystem error: ${err instanceof Error ? err.message : String(err)}. Please check directory permissions or pass a custom file with --annotations.`, "ERR_ANNOTATIONS_FS", "Check directory permissions for the cache directory or specify --annotations <path>.", { cause: err });
|
|
5515
|
+
throw new AnnotationsError(`Failed to resolve nanos world API annotations. Please check your network connection or pass a custom file with --annotations. (${err instanceof Error ? err.message : String(err)})`, "ERR_ANNOTATIONS_NETWORK", "Check your network connection, run 'nanos-lint warmup' when online, or specify --annotations <path>.", { cause: err });
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
//#endregion
|
|
5519
|
+
//#region src/cache-status.ts
|
|
5520
|
+
function getCacheStatus(baseCacheDir = systemPaths.cache) {
|
|
5521
|
+
const totalSize = getDirectorySize(baseCacheDir);
|
|
5522
|
+
const totalSizeFormatted = formatBytes(totalSize);
|
|
5523
|
+
const lualsBaseDir = path.join(baseCacheDir, "luals");
|
|
5524
|
+
const lualsMeta = readLuaLSMetadata(lualsBaseDir);
|
|
5525
|
+
const candidateVersions = listCachedLuaLSVersionDirs(lualsBaseDir);
|
|
5526
|
+
const versionInfos = [];
|
|
5527
|
+
for (const ver of candidateVersions) {
|
|
5528
|
+
const versionDir = getCacheDir(ver, lualsBaseDir);
|
|
5529
|
+
const marker = path.join(versionDir, ".complete");
|
|
5530
|
+
const info = getPlatformInfo(ver);
|
|
5531
|
+
const binPath = path.join(versionDir, info.binaryRelativePath);
|
|
5532
|
+
let isValid = false;
|
|
5533
|
+
if (fs.existsSync(marker) && fs.existsSync(binPath)) try {
|
|
5534
|
+
isValid = fs.readFileSync(marker, "utf-8").trim() === ver && isBinaryValid(binPath);
|
|
5535
|
+
} catch {
|
|
5536
|
+
isValid = false;
|
|
5537
|
+
}
|
|
5538
|
+
const dirSize = getDirectorySize(versionDir);
|
|
5539
|
+
versionInfos.push({
|
|
5540
|
+
version: ver,
|
|
5541
|
+
status: isValid ? "valid" : "corrupted",
|
|
5542
|
+
size: dirSize,
|
|
5543
|
+
sizeFormatted: formatBytes(dirSize),
|
|
5544
|
+
path: versionDir
|
|
5545
|
+
});
|
|
5546
|
+
}
|
|
5547
|
+
const lualsStatus = {
|
|
5548
|
+
weeklyCheck: lualsMeta?.lastCheckedWeek ?? null,
|
|
5549
|
+
lastCheckedDate: lualsMeta?.lastCheckedDate ?? null,
|
|
5550
|
+
targetVersion: lualsMeta?.latestVersion ?? null,
|
|
5551
|
+
versions: versionInfos
|
|
5552
|
+
};
|
|
5553
|
+
const annotationsDir = path.join(baseCacheDir, "annotations");
|
|
5554
|
+
const annotationsFile = path.join(annotationsDir, "annotations.lua");
|
|
5555
|
+
const annMeta = readAnnotationsMetadata(annotationsDir);
|
|
5556
|
+
let annStatus = "missing";
|
|
5557
|
+
let annSize = 0;
|
|
5558
|
+
if (fs.existsSync(annotationsFile)) {
|
|
5559
|
+
annSize = getDirectorySize(annotationsFile);
|
|
5560
|
+
annStatus = isAnnotationsValid(annotationsFile) ? "valid" : "corrupted";
|
|
5561
|
+
}
|
|
5562
|
+
return {
|
|
5563
|
+
cacheDirectory: baseCacheDir,
|
|
5564
|
+
totalSize,
|
|
5565
|
+
totalSizeFormatted,
|
|
5566
|
+
luals: lualsStatus,
|
|
5567
|
+
annotations: {
|
|
5568
|
+
status: annStatus,
|
|
5569
|
+
size: annSize,
|
|
5570
|
+
sizeFormatted: formatBytes(annSize),
|
|
5571
|
+
path: annotationsFile,
|
|
5572
|
+
commitId: annMeta?.commitId ?? null,
|
|
5573
|
+
lastChecked: annMeta?.lastChecked ?? null
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
5576
|
+
}
|
|
5577
|
+
function formatCacheStatusPretty(report) {
|
|
5578
|
+
const lines = [];
|
|
5579
|
+
lines.push("nanos-lint Cache Status");
|
|
5580
|
+
lines.push("");
|
|
5581
|
+
lines.push(`Cache Directory: ${report.cacheDirectory}`);
|
|
5582
|
+
lines.push(`Total Disk Usage: ${report.totalSizeFormatted}`);
|
|
5583
|
+
lines.push("");
|
|
5584
|
+
lines.push("Lua Language Server (LuaLS):");
|
|
5585
|
+
const weeklyStr = report.luals.weeklyCheck ? `${report.luals.weeklyCheck}${report.luals.lastCheckedDate ? ` (last checked: ${report.luals.lastCheckedDate})` : ""}` : "none";
|
|
5586
|
+
lines.push(` - Weekly Check: ${weeklyStr}`);
|
|
5587
|
+
const targetVer = report.luals.targetVersion ?? "none";
|
|
5588
|
+
lines.push(` - Target Version: ${targetVer}`);
|
|
5589
|
+
if (report.luals.versions.length === 0) lines.push(" - Cached Copies: none");
|
|
5590
|
+
else {
|
|
5591
|
+
lines.push(" - Cached Copies:");
|
|
5592
|
+
for (const v of report.luals.versions) lines.push(` * ${v.version} (status: ${v.status}, size: ${v.sizeFormatted})`);
|
|
5593
|
+
}
|
|
5594
|
+
lines.push("");
|
|
5595
|
+
lines.push("nanos world Annotations:");
|
|
5596
|
+
if (report.annotations.status === "missing") lines.push(" - Status: Not cached");
|
|
5597
|
+
else {
|
|
5598
|
+
const statusCap = report.annotations.status === "valid" ? "Valid" : "Corrupted";
|
|
5599
|
+
lines.push(` - Status: ${statusCap} (${report.annotations.sizeFormatted})`);
|
|
5600
|
+
if (report.annotations.commitId) {
|
|
5601
|
+
const sha = report.annotations.commitId;
|
|
5602
|
+
const shaDisplay = sha.length >= 7 && sha !== "unknown" ? `${sha.slice(0, 8)} (docgen-output)` : sha;
|
|
5603
|
+
lines.push(` - Commit SHA: ${shaDisplay}`);
|
|
5604
|
+
}
|
|
5605
|
+
if (report.annotations.lastChecked) lines.push(` - Last Checked: ${report.annotations.lastChecked}`);
|
|
5211
5606
|
}
|
|
5607
|
+
return lines.join("\n");
|
|
5212
5608
|
}
|
|
5213
5609
|
//#endregion
|
|
5214
5610
|
//#region src/cli.ts
|
|
@@ -5278,7 +5674,7 @@ function createProgram(options) {
|
|
|
5278
5674
|
};
|
|
5279
5675
|
if (opts.config) {
|
|
5280
5676
|
const resolvedConfig = path.resolve(opts.config);
|
|
5281
|
-
if (!fs.existsSync(resolvedConfig)) throw new
|
|
5677
|
+
if (!fs.existsSync(resolvedConfig)) throw new ConfigError(`Configuration file not found: ${resolvedConfig}`, "ERR_CONFIG_NOT_FOUND", "Verify the path passed to --config exists and is readable.");
|
|
5282
5678
|
}
|
|
5283
5679
|
const annotationsPath = await resolveAnnotations({
|
|
5284
5680
|
customPath: opts.annotations,
|
|
@@ -5311,13 +5707,24 @@ function createProgram(options) {
|
|
|
5311
5707
|
})}`);
|
|
5312
5708
|
setExitCode(0);
|
|
5313
5709
|
});
|
|
5710
|
+
program.command("warmup").alias("download").description("Pre-fetch and cache both LuaLS binary and annotations for offline execution").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).option("--annotations <path>", "Path to custom annotations.lua file").option("-q, --quiet", "Suppress download progress logging").action(async (opts) => {
|
|
5711
|
+
writeOutput(`[warmup] LuaLS binary ready: ${await resolveLuaLSBinary(opts?.lualsVersion || "latest", { quiet: opts?.quiet })}`);
|
|
5712
|
+
const annotationsPath = await resolveAnnotations({
|
|
5713
|
+
customPath: opts?.annotations,
|
|
5714
|
+
quiet: opts?.quiet
|
|
5715
|
+
});
|
|
5716
|
+
const meta = readAnnotationsMetadata();
|
|
5717
|
+
writeOutput(`[warmup] nanos world annotations ready: ${annotationsPath}${meta?.commitId && meta.commitId !== "unknown" ? ` (commit ${meta.commitId.slice(0, 7)})` : ""}`);
|
|
5718
|
+
writeOutput("[warmup] Cache pre-warmed successfully. Ready for offline execution.");
|
|
5719
|
+
setExitCode(0);
|
|
5720
|
+
});
|
|
5314
5721
|
program.command("download-luals [version]").description("Download and cache the LuaLS binary").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).action(async (version, opts) => {
|
|
5315
5722
|
const ver = version || opts?.lualsVersion || "latest";
|
|
5316
5723
|
writeOutput(`[luals] Downloading LuaLS ${ver}...`);
|
|
5317
5724
|
writeOutput(`[luals] Ready at: ${await resolveLuaLSBinary(ver)}`);
|
|
5318
5725
|
setExitCode(0);
|
|
5319
5726
|
});
|
|
5320
|
-
|
|
5727
|
+
const handleCleanCache = () => {
|
|
5321
5728
|
try {
|
|
5322
5729
|
const cleared = cleanCache();
|
|
5323
5730
|
if (cleared) writeOutput(`[cache] Cleared cache at: ${cleared}`);
|
|
@@ -5327,6 +5734,26 @@ function createProgram(options) {
|
|
|
5327
5734
|
logger.error(`[cache] Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
5328
5735
|
setExitCode(1);
|
|
5329
5736
|
}
|
|
5737
|
+
};
|
|
5738
|
+
const handleCacheStatus = (opts) => {
|
|
5739
|
+
const report = getCacheStatus();
|
|
5740
|
+
if (opts?.json) writeOutput(JSON.stringify(report, null, 2));
|
|
5741
|
+
else writeOutput(formatCacheStatusPretty(report));
|
|
5742
|
+
setExitCode(0);
|
|
5743
|
+
};
|
|
5744
|
+
const cacheCmd = program.command("cache").description("Inspect or manage the nanos-lint cache").option("--json", "Output cache state in JSON format");
|
|
5745
|
+
cacheCmd.command("status", { isDefault: true }).alias("info").description("Show cache status, installed versions, and disk usage").option("--json", "Output cache state in JSON format").action((opts, cmd) => {
|
|
5746
|
+
const json = opts?.json || (cmd.parent ? Boolean(cmd.parent.opts()?.json) : false);
|
|
5747
|
+
handleCacheStatus({ json });
|
|
5748
|
+
});
|
|
5749
|
+
cacheCmd.command("clean").description("Clear the nanos-lint cache").action(() => {
|
|
5750
|
+
handleCleanCache();
|
|
5751
|
+
});
|
|
5752
|
+
program.command("cache-status").description("Show cache status, installed versions, and disk usage").option("--json", "Output cache state in JSON format").action((opts) => {
|
|
5753
|
+
handleCacheStatus(opts);
|
|
5754
|
+
});
|
|
5755
|
+
program.command("clean-cache").alias("clean").description("Clear the nanos-lint cache").action(() => {
|
|
5756
|
+
handleCleanCache();
|
|
5330
5757
|
});
|
|
5331
5758
|
program.command("version").description("Show version information").action(() => {
|
|
5332
5759
|
writeOutput(getVersionString());
|
|
@@ -5336,6 +5763,8 @@ function createProgram(options) {
|
|
|
5336
5763
|
program.addHelpText("after", `
|
|
5337
5764
|
Examples:
|
|
5338
5765
|
$ npx nanos-lint
|
|
5766
|
+
$ npx nanos-lint warmup
|
|
5767
|
+
$ npx nanos-lint cache status
|
|
5339
5768
|
$ npx nanos-lint check ./my-package
|
|
5340
5769
|
$ npx nanos-lint check . --checklevel=Error
|
|
5341
5770
|
$ npx nanos-lint check . --ignore "myfolder/hello-*.lua"
|
|
@@ -5347,8 +5776,10 @@ Examples:
|
|
|
5347
5776
|
async function runCLI(args = process.argv.slice(2)) {
|
|
5348
5777
|
for (let i = 0; i < args.length; i++) {
|
|
5349
5778
|
const arg = args[i];
|
|
5779
|
+
if (!arg) continue;
|
|
5350
5780
|
if (arg === "-l" || arg === "--log-level") {
|
|
5351
|
-
|
|
5781
|
+
const nextArg = args[i + 1];
|
|
5782
|
+
if (nextArg && isValidLogLevel(nextArg)) logger.setLevel(nextArg);
|
|
5352
5783
|
} else if (arg.startsWith("--log-level=")) {
|
|
5353
5784
|
const val = arg.split("=")[1];
|
|
5354
5785
|
if (val && isValidLogLevel(val)) logger.setLevel(val);
|
|
@@ -5363,9 +5794,18 @@ async function runCLI(args = process.argv.slice(2)) {
|
|
|
5363
5794
|
return exitCode;
|
|
5364
5795
|
} catch (err) {
|
|
5365
5796
|
if (err instanceof CommanderError) return err.exitCode;
|
|
5797
|
+
if (err instanceof NanosLintError) {
|
|
5798
|
+
logger.error(`error: ${err.message}`);
|
|
5799
|
+
if (err.remedy) logger.error(`hint: ${err.remedy}`);
|
|
5800
|
+
if ((logger.getLevel() === "debug" || process.env.DEBUG) && err.stack) {
|
|
5801
|
+
logger.error(err.stack);
|
|
5802
|
+
if (err.cause) logger.error(`cause: ${err.cause instanceof Error ? err.cause.stack || err.cause.message : String(err.cause)}`);
|
|
5803
|
+
}
|
|
5804
|
+
return 1;
|
|
5805
|
+
}
|
|
5366
5806
|
const message = err instanceof Error ? err.message : String(err);
|
|
5367
5807
|
logger.error(`error: ${message}`);
|
|
5368
|
-
if (process.env.DEBUG && err instanceof Error && err.stack) logger.error(err.stack);
|
|
5808
|
+
if ((logger.getLevel() === "debug" || process.env.DEBUG) && err instanceof Error && err.stack) logger.error(err.stack);
|
|
5369
5809
|
return 1;
|
|
5370
5810
|
}
|
|
5371
5811
|
}
|
|
@@ -5405,6 +5845,6 @@ if (isDirectExecution()) runCLI().then((code) => {
|
|
|
5405
5845
|
process.exit(1);
|
|
5406
5846
|
});
|
|
5407
5847
|
//#endregion
|
|
5408
|
-
export {
|
|
5848
|
+
export { fetchLatestLuaLSVersionFromGitHub as $, pluralize as A, logger as At, getBaseLuaLSCacheDir as B, updateLastCheckedDate as C, systemPaths as Ct, formatReport as D, LOG_LEVEL_PRIORITY as Dt, formatProblemSummary as E, LOG_LEVELS as Et, downloadAndExtractLuaLS as F, listCachedLuaLSVersionDirs as G, getIsoWeek as H, escapePowerShellSingleQuote as I, writeLuaLSMetadata as J, listCachedLuaLSVersions as K, LUALS_METADATA_FILENAME as L, resolveLuaLSBinary as M, runLuaLSCheck as N, formatSeverityBadge as O, Logger as Ot, countCheckedFiles as P, FALLBACK_LUALS_VERSION as Q, cleanupOldCachedLuaLSVersions as R, resolveAnnotations as S, getDirectorySize as St, formatPretty as T, DEFAULT_LOG_LEVEL as Tt, getLegacyCacheDir as U, getCacheDir as V, getLuaLSMetadataPath as W, getPlatformInfo as X, isBinaryValid as Y, DEFAULT_LUALS_VERSION as Z, getAnnotationsMetadataFilePath as _, ConfigError as _t, formatCacheStatusPretty as a, getDefaultTemplatePath as at, isAnnotationsValid as b, cleanCache as bt, DOCGEN_BRANCH as c, initWorkspace as ct, METADATA_FILENAME as d, parseJsonc as dt, resolveLatestLuaLSVersion as et, RAW_ANNOTATIONS_URL as f, resolveWorkspaceConfig as ft, getAnnotationsCacheDir as g, CacheError as gt, fetchRawAnnotationsContent as h, AnnotationsError as ht, runCLI as i, getDefaultAnnotationsPath as it, shouldEnableColor as j, parseLogLevel as jt, getColors as k, isValidLogLevel as kt, DOCGEN_REPO as l, loadConfigFile as lt, fetchLatestCommitId as m, stripTrailingSlashes as mt, createProgram as n, sanitizeLuaLSVersion as nt, getCacheStatus as o, getDefinitionsDir as ot, downloadAndCacheAnnotations as p, stripJsonComments as pt, readLuaLSMetadata as q, isDirectExecution as r, VALID_LUALS_DIAGNOSTIC_CODES as rt, ANNOTATIONS_FILENAME as s, getPackageRoot as st, collectIgnorePatterns as t, resolveLuaLSVersion as tt, GITHUB_COMMITS_API as u, mergeConfigs as ut, getCachedAnnotationsFilePath as v, LuaLSError as vt, formatGitHubAnnotations as w, fileUriToPath as wt, readAnnotationsMetadata as x, formatBytes as xt, getTodayDateString as y, NanosLintError as yt, findExistingLuaLSDir as z };
|
|
5409
5849
|
|
|
5410
|
-
//# sourceMappingURL=cli-
|
|
5850
|
+
//# sourceMappingURL=cli-DGez8MSI.js.map
|