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.
@@ -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 Error(`Configuration file not found: ${filePath}`);
984
- return parseJsonc(fs.readFileSync(filePath, "utf-8"));
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 mergedSeverity = {
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 Error(`.luarc.json already exists at ${targetFile}. Use --force to overwrite.`);
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 Error(`Definitions file not found at ${sourceAnnotations}. Make sure annotations have been downloaded or pass a valid file with --annotations.`);
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 Error(`Invalid LuaLS version: "${version}". Expected a release tag such as "3.19.1", or "latest".`);
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 Error(`Unsupported Windows architecture: ${arch}. Supported: x64`);
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 Error(`Unsupported Linux architecture: ${arch}. Supported: x64, arm64`);
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 Error(`Unsupported platform: ${platform}`);
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
- const legacyBin = path.join(legacyCache, info.binaryRelativePath);
1436
- const legacyMarker = path.join(legacyCache, ".complete");
1437
- if (fs.existsSync(legacyMarker)) try {
1438
- if (fs.readFileSync(legacyMarker, "utf-8").trim() === version && isBinaryValid(legacyBin)) return legacyCache;
1439
- } catch (err) {
1440
- logger.debug(`[luals] Error checking legacy LuaLS cache marker at ${legacyMarker}: ${err instanceof Error ? err.message : String(err)}`);
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
- * Verifies that a LuaLS binary exists, has non-trivial size, and is executable.
1661
+ * Escapes single quotes for safe PowerShell single-quoted string interpolation.
1449
1662
  */
1450
- function isBinaryValid(binaryPath) {
1451
- if (!fs.existsSync(binaryPath)) return false;
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) throw lastErr || /* @__PURE__ */ new Error(`Failed to download ${url}`);
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 Error(`Failed to extract valid LuaLS binary to expected path: ${tempBinaryPath}`);
1549
- if (!isBinaryValid(tempBinaryPath)) throw new Error(`Extracted LuaLS binary at ${tempBinaryPath} is invalid or non-functional.`);
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
- const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
1606
- if (fs.existsSync(bundledPath) && isBinaryValid(bundledPath)) return bundledPath;
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)) return 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 (!options?.quiet) logger.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
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
- const legacyDir = getLegacyCacheDir(resolvedVersion);
1627
- const legacyPath = path.join(legacyDir, info.binaryRelativePath);
1628
- const legacyMarker = path.join(legacyDir, ".complete");
1629
- if (fs.existsSync(legacyPath)) {
1630
- let validLegacy = false;
1631
- if (fs.existsSync(legacyMarker)) try {
1632
- if (fs.readFileSync(legacyMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(legacyPath)) validLegacy = true;
1633
- } catch (err) {
1634
- logger.debug(`[luals] Failed to read legacy complete marker at ${legacyMarker}: ${err instanceof Error ? err.message : String(err)}`);
1635
- }
1636
- else if (isBinaryValid(legacyPath)) validLegacy = true;
1637
- if (validLegacy) {
1638
- if (path.resolve(legacyDir) !== path.resolve(cachedDir)) try {
1639
- fs.mkdirSync(path.dirname(cachedDir), { recursive: true });
1640
- fs.cpSync(legacyDir, cachedDir, { recursive: true });
1641
- if (fs.existsSync(cachedPath) && isBinaryValid(cachedPath)) return cachedPath;
1642
- } catch (err) {
1643
- logger.warn(`[luals] Failed to migrate legacy cache from ${legacyDir} to ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
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
- return await downloadAndExtractLuaLS(resolvedVersion, getCacheDir(resolvedVersion, baseCacheDir), options);
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
- if (metadata && metadata.lastCheckedWeek === currentWeek && cachedVersions.length > 0) {
1667
- const fallbackVersion = cachedVersions[0];
1668
- const info = getPlatformInfo(fallbackVersion);
1669
- return path.join(getCacheDir(fallbackVersion, baseCacheDir), info.binaryRelativePath);
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 (cachedVersions.length > 0) {
1679
- logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${cachedVersions[0]}.`);
1680
- targetVersion = cachedVersions[0];
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
- if (fs.existsSync(completeMarker) && isBinaryValid(targetBinaryPath)) {
1692
- cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
1693
- return targetBinaryPath;
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
- const downloadedBinary = await downloadAndExtractLuaLS(targetVersion, targetCacheDir, options);
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
- const absoluteTarget = path.resolve(targetPath);
1709
- if (!fs.existsSync(absoluteTarget)) throw new Error(`Target path does not exist: ${targetPath}`);
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 Error(`LuaLS check failed to execute or produce diagnostic output: ${execError instanceof Error ? execError.message : String(execError)}. ${cacheHint}`);
1755
- throw new Error(`LuaLS check failed to produce diagnostic output at: ${checkOutPath}. ${cacheHint}`);
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 normTarget = path.resolve(targetFileOnly).toLowerCase();
1760
- for (const [rawUri, diags] of Object.entries(diagnostics)) {
1761
- const filePath = fileUriToPath(rawUri);
1762
- if (path.resolve(filePath).toLowerCase() === normTarget) filtered[rawUri] = diags;
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 Error(`Failed to download annotations.lua: ${res.status} ${res.statusText}`);
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 Error("Downloaded annotations.lua appears truncated or invalid");
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 Error(`Custom annotations file not found: ${resolved}`);
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 Error(`Annotations file specified in environment not found: ${resolved}`);
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 (metadata && metadata.lastChecked === dateStr && fs.existsSync(cachedAnnotationsFile)) return cachedAnnotationsFile;
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 Error(`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.`, { cause: err });
5210
- throw new Error(`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)})`, { cause: err });
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 Error(`Configuration file not found: ${resolvedConfig}`);
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
- program.command("clean-cache").alias("clean").description("Clear the nanos-lint cache").action(() => {
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
- if (i + 1 < args.length && isValidLogLevel(args[i + 1])) logger.setLevel(args[i + 1]);
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 { getDefaultAnnotationsPath as $, FALLBACK_LUALS_VERSION as A, getIsoWeek as B, formatProblemSummary as C, pluralize as D, getColors as E, escapePowerShellSingleQuote as F, listCachedLuaLSVersions as G, getLuaLSMetadataPath as H, fetchLatestLuaLSVersionFromGitHub as I, resolveLuaLSBinary as J, readLuaLSMetadata as K, findExistingLuaLSDir as L, cleanupOldCachedLuaLSVersions as M, countCheckedFiles as N, shouldEnableColor as O, downloadAndExtractLuaLS as P, writeLuaLSMetadata as Q, getBaseLuaLSCacheDir as R, formatPretty as S, formatSeverityBadge as T, getPlatformInfo as U, getLegacyCacheDir as V, isBinaryValid as W, runLuaLSCheck as X, resolveLuaLSVersion as Y, sanitizeLuaLSVersion as Z, getTodayDateString as _, isValidLogLevel as _t, ANNOTATIONS_FILENAME as a, mergeConfigs as at, updateLastCheckedDate as b, GITHUB_COMMITS_API as c, stripJsonComments as ct, downloadAndCacheAnnotations as d, systemPaths as dt, getDefaultTemplatePath as et, fetchLatestCommitId as f, fileUriToPath as ft, getCachedAnnotationsFilePath as g, Logger as gt, getAnnotationsMetadataFilePath as h, LOG_LEVEL_PRIORITY as ht, runCLI as i, loadConfigFile as it, LUALS_METADATA_FILENAME as j, DEFAULT_LUALS_VERSION as k, METADATA_FILENAME as l, stripTrailingSlashes as lt, getAnnotationsCacheDir as m, LOG_LEVELS as mt, createProgram as n, getPackageRoot as nt, DOCGEN_BRANCH as o, parseJsonc as ot, fetchRawAnnotationsContent as p, DEFAULT_LOG_LEVEL as pt, resolveLatestLuaLSVersion as q, isDirectExecution as r, initWorkspace as rt, DOCGEN_REPO as s, resolveWorkspaceConfig as st, collectIgnorePatterns as t, getDefinitionsDir as tt, RAW_ANNOTATIONS_URL as u, cleanCache as ut, readAnnotationsMetadata as v, logger as vt, formatReport as w, formatGitHubAnnotations as x, resolveAnnotations as y, parseLogLevel as yt, getCacheDir as z };
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-sEO_MZ-K.js.map
5850
+ //# sourceMappingURL=cli-DGez8MSI.js.map