nanos-lint 2.4.0 → 2.6.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.
@@ -2,10 +2,10 @@ import { fileURLToPath } from "node:url";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
+ import process$1 from "node:process";
5
6
  import childProcess, { execFile, execFileSync } from "node:child_process";
6
7
  import { promisify, stripVTControlCharacters } from "node:util";
7
8
  import { EventEmitter } from "node:events";
8
- import process$1 from "node:process";
9
9
  //#region src/types.ts
10
10
  /**
11
11
  * Converts a URI (e.g. `file:///path/to/file` or `file:///C:/path/to/file` or `file:///c%3A/path` or `file://server/share/file`)
@@ -778,6 +778,93 @@ function printParseErrorCode(code) {
778
778
  }
779
779
  return "<unknown ParseErrorCode>";
780
780
  }
781
+ Object.freeze([
782
+ "",
783
+ " ",
784
+ ".",
785
+ "..",
786
+ " .",
787
+ ". ",
788
+ " ..",
789
+ ".. ",
790
+ "../",
791
+ "../foo",
792
+ "foo/../bar",
793
+ "foo/bar",
794
+ "foo\\bar",
795
+ "foo\0bar"
796
+ ]);
797
+ function isSafeFilename(filename) {
798
+ if (typeof filename !== "string") return false;
799
+ const trimmed = filename.trim();
800
+ return trimmed !== "" && trimmed !== "." && trimmed !== ".." && !filename.includes("/") && !filename.includes("\\") && !filename.includes("\0");
801
+ }
802
+ function assertSafeFilename(filename) {
803
+ if (typeof filename !== "string") throw new TypeError("Expected a string");
804
+ if (!isSafeFilename(filename)) throw new Error(`Unsafe filename: ${JSON.stringify(filename)}`);
805
+ }
806
+ //#endregion
807
+ //#region node_modules/env-paths/index.js
808
+ const homedir = os.homedir();
809
+ const tmpdir = os.tmpdir();
810
+ const { env } = process$1;
811
+ const macos = (name) => {
812
+ const library = path.join(homedir, "Library");
813
+ return {
814
+ data: path.join(library, "Application Support", name),
815
+ config: path.join(library, "Preferences", name),
816
+ cache: path.join(library, "Caches", name),
817
+ log: path.join(library, "Logs", name),
818
+ temp: path.join(tmpdir, name)
819
+ };
820
+ };
821
+ const windows = (name) => {
822
+ const appData = env.APPDATA || path.join(homedir, "AppData", "Roaming");
823
+ const localAppData = env.LOCALAPPDATA || path.join(homedir, "AppData", "Local");
824
+ return {
825
+ data: path.join(localAppData, name, "Data"),
826
+ config: path.join(appData, name, "Config"),
827
+ cache: path.join(localAppData, name, "Cache"),
828
+ log: path.join(localAppData, name, "Log"),
829
+ temp: path.join(tmpdir, name)
830
+ };
831
+ };
832
+ const linux = (name) => {
833
+ const username = path.basename(homedir);
834
+ return {
835
+ data: path.join(env.XDG_DATA_HOME || path.join(homedir, ".local", "share"), name),
836
+ config: path.join(env.XDG_CONFIG_HOME || path.join(homedir, ".config"), name),
837
+ cache: path.join(env.XDG_CACHE_HOME || path.join(homedir, ".cache"), name),
838
+ log: path.join(env.XDG_STATE_HOME || path.join(homedir, ".local", "state"), name),
839
+ temp: path.join(tmpdir, username, name)
840
+ };
841
+ };
842
+ function envPaths(name, { suffix = "nodejs" } = {}) {
843
+ assertSafeFilename(name);
844
+ if (suffix) name += `-${suffix}`;
845
+ assertSafeFilename(name);
846
+ if (process$1.platform === "darwin") return macos(name);
847
+ if (process$1.platform === "win32") return windows(name);
848
+ return linux(name);
849
+ }
850
+ //#endregion
851
+ //#region src/paths.ts
852
+ /**
853
+ * System paths resolved for nanos-lint using cross-platform conventions
854
+ * provided by env-paths (XDG on Linux, %LOCALAPPDATA%/%APPDATA% on Windows, ~/Library on macOS).
855
+ */
856
+ const systemPaths = envPaths("nanos-lint", { suffix: "" });
857
+ /**
858
+ * Clears the nanos-lint cache directory.
859
+ * Returns the path of the cleared cache directory, or null if it did not exist.
860
+ */
861
+ function cleanCache(targetDir = systemPaths.cache) {
862
+ if (fs.existsSync(targetDir)) {
863
+ fs.rmSync(targetDir, { recursive: true });
864
+ return targetDir;
865
+ }
866
+ return null;
867
+ }
781
868
  //#endregion
782
869
  //#region src/config.ts
783
870
  const __filename = fileURLToPath(import.meta.url);
@@ -793,9 +880,19 @@ function getPackageRoot() {
793
880
  }
794
881
  return path.resolve(__dirname, "..");
795
882
  }
883
+ function getDefaultAnnotationsPath() {
884
+ const envPath = process.env.NANOS_ANNOTATIONS_PATH || process.env.NANOS_ANNOTATIONS;
885
+ if (envPath && fs.existsSync(envPath)) return path.resolve(envPath);
886
+ const bundled = path.join(getPackageRoot(), "annotations.lua");
887
+ if (fs.existsSync(bundled)) return bundled;
888
+ return path.join(systemPaths.cache, "annotations", "annotations.lua");
889
+ }
890
+ /**
891
+ * @deprecated Use getDefaultAnnotationsPath() instead.
892
+ * Returns the directory containing the resolved annotations.lua file.
893
+ */
796
894
  function getDefinitionsDir() {
797
- const root = getPackageRoot();
798
- return path.join(root, "vendor", "nanos-world-vscode-extension");
895
+ return path.dirname(getDefaultAnnotationsPath());
799
896
  }
800
897
  function getDefaultTemplatePath() {
801
898
  const root = getPackageRoot();
@@ -823,16 +920,41 @@ function loadConfigFile(filePath) {
823
920
  return parseJsonc(fs.readFileSync(filePath, "utf-8"));
824
921
  }
825
922
  /**
923
+ * Removes trailing `/` characters from a path-like string.
924
+ *
925
+ * Implemented with a scan instead of a `\/+$` regular expression: on inputs made
926
+ * of many slashes that do not end in a slash, a backtracking engine retries the
927
+ * repetition at every offset, which is quadratic in the input length
928
+ * (CodeQL: js/polynomial-redos).
929
+ */
930
+ function stripTrailingSlashes(value) {
931
+ let end = value.length;
932
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
933
+ return end === value.length ? value : value.slice(0, end);
934
+ }
935
+ /**
826
936
  * Merges a base nanos configuration with a workspace override configuration.
827
- * Guarantees that the nanos definitions directory is included in workspace.library,
937
+ * Guarantees that nanos API annotations are included in workspace.library,
828
938
  * and standardizes paths for LuaLS.
939
+ *
940
+ * @param base Base configuration template
941
+ * @param override Workspace override configuration
942
+ * @param definitionsPath Path to annotations.lua file or directory containing it
943
+ * @param options Additional merge options
829
944
  */
830
- function mergeConfigs(base, override = {}, definitionsDir = getDefinitionsDir(), options) {
831
- const normalizedDefDir = definitionsDir.split(path.sep).join("/");
945
+ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotationsPath(), options) {
946
+ let resolvedPath = definitionsPath;
947
+ if (fs.existsSync(definitionsPath)) try {
948
+ if (fs.statSync(definitionsPath).isDirectory()) {
949
+ const candidate = path.join(definitionsPath, "annotations.lua");
950
+ if (fs.existsSync(candidate)) resolvedPath = candidate;
951
+ }
952
+ } catch {}
953
+ const normalizedDefPath = resolvedPath.split(path.sep).join("/");
832
954
  const baseLibraries = base.workspace?.library ?? [];
833
955
  const overrideLibraries = override.workspace?.library ?? [];
834
956
  const librarySet = /* @__PURE__ */ new Set([
835
- normalizedDefDir,
957
+ normalizedDefPath,
836
958
  ...baseLibraries,
837
959
  ...overrideLibraries
838
960
  ]);
@@ -869,12 +991,12 @@ function mergeConfigs(base, override = {}, definitionsDir = getDefinitionsDir(),
869
991
  for (const pat of normalizedCliIgnore) {
870
992
  excludePatterns.add(pat);
871
993
  if (!pat.includes("*") && !pat.includes("?") && !pat.endsWith(".lua")) {
872
- const dirPat = pat.replace(/\/+$/, "");
994
+ const dirPat = stripTrailingSlashes(pat);
873
995
  excludePatterns.add(`${dirPat}/**`);
874
996
  }
875
997
  }
876
998
  mergedFilesExclude = Array.from(excludePatterns);
877
- const cliDirs = normalizedCliIgnore.filter((p) => !p.includes("*") && !p.includes("?") && !p.endsWith(".lua")).map((p) => p.replace(/\/+$/, ""));
999
+ const cliDirs = normalizedCliIgnore.filter((p) => !p.includes("*") && !p.includes("?") && !p.endsWith(".lua")).map((p) => stripTrailingSlashes(p));
878
1000
  mergedIgnoreDir = Array.from(/* @__PURE__ */ new Set([
879
1001
  ...defaultIgnore,
880
1002
  ...baseIgnore,
@@ -925,7 +1047,7 @@ function mergeConfigs(base, override = {}, definitionsDir = getDefinitionsDir(),
925
1047
  */
926
1048
  function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
927
1049
  const defaultTemplate = loadConfigFile(getDefaultTemplatePath());
928
- const definitionsDir = getDefinitionsDir();
1050
+ const activeAnnotationsPath = options?.annotationsPath || getDefaultAnnotationsPath();
929
1051
  let userConfig = {};
930
1052
  if (customConfigPath) userConfig = loadConfigFile(path.resolve(customConfigPath));
931
1053
  else {
@@ -939,7 +1061,7 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
939
1061
  const hasCliIgnore = Boolean(options?.ignore && options.ignore.length > 0);
940
1062
  let cliIgnore = options?.ignore;
941
1063
  if (hasCliIgnore && cliIgnore) {
942
- const normWs = workspacePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1064
+ const normWs = stripTrailingSlashes(workspacePath.replace(/\\/g, "/").replace(/^\.\//, ""));
943
1065
  const expanded = [];
944
1066
  for (const pat of cliIgnore) {
945
1067
  expanded.push(pat);
@@ -948,7 +1070,7 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
948
1070
  }
949
1071
  cliIgnore = expanded;
950
1072
  }
951
- const merged = mergeConfigs(defaultTemplate, userConfig, definitionsDir, { cliIgnore });
1073
+ const merged = mergeConfigs(defaultTemplate, userConfig, activeAnnotationsPath, { cliIgnore });
952
1074
  if (!hasCliIgnore) {
953
1075
  const resolvedTarget = path.resolve(workspacePath);
954
1076
  if (fs.existsSync(path.join(resolvedTarget, "main.lua")) && (fs.existsSync(path.join(resolvedTarget, "bin", "lua-language-server.exe")) || fs.existsSync(path.join(resolvedTarget, "bin", "lua-language-server")))) {
@@ -963,7 +1085,7 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
963
1085
  ])];
964
1086
  }
965
1087
  }
966
- const tempDir = path.join(os.tmpdir(), "nanos-lint");
1088
+ const tempDir = systemPaths.temp;
967
1089
  fs.mkdirSync(tempDir, { recursive: true });
968
1090
  const tempConfigFile = path.join(tempDir, `luarc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
969
1091
  fs.writeFileSync(tempConfigFile, JSON.stringify(merged, null, 2), "utf-8");
@@ -979,9 +1101,8 @@ function initWorkspace(workspacePath, options) {
979
1101
  const targetFile = path.join(workspacePath, ".luarc.json");
980
1102
  if (fs.existsSync(targetFile) && !options?.force) throw new Error(`.luarc.json already exists at ${targetFile}. Use --force to overwrite.`);
981
1103
  const template = loadConfigFile(getDefaultTemplatePath());
982
- const definitionsDir = getDefinitionsDir();
983
- const sourceAnnotations = path.join(definitionsDir, "annotations.lua");
984
- if (!fs.existsSync(sourceAnnotations)) throw new Error(`Definitions file not found at ${sourceAnnotations}. Make sure submodules are initialized.`);
1104
+ const sourceAnnotations = options?.annotationsPath || getDefaultAnnotationsPath();
1105
+ 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.`);
985
1106
  const targetNanosDir = path.join(workspacePath, ".nanos-lint");
986
1107
  fs.mkdirSync(targetNanosDir, { recursive: true });
987
1108
  const targetAnnotations = path.join(targetNanosDir, "annotations.lua");
@@ -1002,6 +1123,39 @@ const execFileAsync = promisify(execFile);
1002
1123
  const FALLBACK_LUALS_VERSION = "3.19.1";
1003
1124
  const DEFAULT_LUALS_VERSION = "latest";
1004
1125
  /**
1126
+ * Characters accepted in a LuaLS version/tag. Only ASCII letters, digits, dots,
1127
+ * dashes and underscores are allowed, so a version can never contain a path
1128
+ * separator, a drive letter or a traversal segment.
1129
+ */
1130
+ const SAFE_VERSION_CHARS = new Map([..."0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._-"].map((ch) => [ch, ch]));
1131
+ const MAX_VERSION_LENGTH = 64;
1132
+ /**
1133
+ * Validates a LuaLS version/tag and rebuilds it from the allow-list above.
1134
+ *
1135
+ * Version strings originate from untrusted sources: the GitHub releases API
1136
+ * response and user supplied `--luals-version` arguments. They are interpolated
1137
+ * into cache directory paths, download URLs, and the path of the binary that is
1138
+ * eventually executed, so they must be constrained to a single safe path
1139
+ * segment. Rebuilding the value character by character guarantees the returned
1140
+ * string only ever contains allow-listed characters (CodeQL: js/command-line-injection).
1141
+ *
1142
+ * @returns the normalized version (a single leading `v` is dropped), or `null`
1143
+ * when the input cannot be used as a version tag.
1144
+ */
1145
+ function sanitizeLuaLSVersion(raw) {
1146
+ const trimmed = raw.trim();
1147
+ if (trimmed.length === 0 || trimmed.length > MAX_VERSION_LENGTH) return null;
1148
+ let version = "";
1149
+ for (const ch of trimmed) {
1150
+ const allowed = SAFE_VERSION_CHARS.get(ch);
1151
+ if (allowed === void 0) return null;
1152
+ version += allowed;
1153
+ }
1154
+ if (version.charCodeAt(0) === 118) version = version.slice(1);
1155
+ const first = version.charCodeAt(0);
1156
+ return first >= 48 && first <= 57 || first >= 65 && first <= 90 || first >= 97 && first <= 122 ? version : null;
1157
+ }
1158
+ /**
1005
1159
  * Escapes single quotes for safe PowerShell single-quoted string interpolation.
1006
1160
  */
1007
1161
  function escapePowerShellSingleQuote(str) {
@@ -1020,17 +1174,22 @@ async function resolveLatestLuaLSVersion() {
1020
1174
  });
1021
1175
  if (res.ok) {
1022
1176
  const data = await res.json();
1023
- if (data.tag_name) return data.tag_name.replace(/^v/, "");
1177
+ const version = typeof data.tag_name === "string" ? sanitizeLuaLSVersion(data.tag_name) : null;
1178
+ if (version) return version;
1024
1179
  }
1025
1180
  } catch {}
1026
1181
  return FALLBACK_LUALS_VERSION;
1027
1182
  }
1028
1183
  /**
1029
1184
  * Resolves a version string ("latest" -> actual tag).
1185
+ *
1186
+ * @throws when an explicitly requested version is not a valid tag.
1030
1187
  */
1031
1188
  async function resolveLuaLSVersion(version) {
1032
1189
  if (!version || version === "latest") return await resolveLatestLuaLSVersion();
1033
- return version.replace(/^v/, "");
1190
+ const sanitized = sanitizeLuaLSVersion(version);
1191
+ if (!sanitized) throw new Error(`Invalid LuaLS version: "${version}". Expected a release tag such as "3.19.1", or "latest".`);
1192
+ return sanitized;
1034
1193
  }
1035
1194
  function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
1036
1195
  const platform = process.platform;
@@ -1068,6 +1227,12 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
1068
1227
  throw new Error(`Unsupported platform: ${platform}`);
1069
1228
  }
1070
1229
  function getCacheDir(version = FALLBACK_LUALS_VERSION) {
1230
+ return path.join(systemPaths.cache, "luals", version);
1231
+ }
1232
+ /**
1233
+ * Returns the legacy cache directory used in nanos-lint <= 2.2.1.
1234
+ */
1235
+ function getLegacyCacheDir(version = FALLBACK_LUALS_VERSION) {
1071
1236
  const base = process.platform === "win32" ? process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
1072
1237
  return path.join(base, "nanos-lint", "luals", version);
1073
1238
  }
@@ -1096,12 +1261,9 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1096
1261
  const binaryPath = path.join(destDir, info.binaryRelativePath);
1097
1262
  const completeMarker = path.join(destDir, ".complete");
1098
1263
  if (fs.existsSync(destDir)) {
1099
- if (fs.existsSync(binaryPath) && isBinaryValid(binaryPath)) {
1100
- if (!fs.existsSync(completeMarker)) try {
1101
- fs.writeFileSync(completeMarker, resolvedVersion, "utf-8");
1102
- } catch {}
1103
- return binaryPath;
1104
- }
1264
+ if (fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
1265
+ if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(binaryPath)) return binaryPath;
1266
+ } catch {}
1105
1267
  try {
1106
1268
  fs.rmSync(destDir, {
1107
1269
  recursive: true,
@@ -1168,6 +1330,7 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1168
1330
  fs.chmodSync(tempBinaryPath, 493);
1169
1331
  } catch {}
1170
1332
  if (!fs.existsSync(tempBinaryPath) || fs.statSync(tempBinaryPath).size < 1e5) throw new Error(`Failed to extract valid LuaLS binary to expected path: ${tempBinaryPath}`);
1333
+ if (!isBinaryValid(tempBinaryPath)) throw new Error(`Extracted LuaLS binary at ${tempBinaryPath} is invalid or non-functional.`);
1171
1334
  fs.writeFileSync(path.join(tempDir, ".complete"), resolvedVersion, "utf-8");
1172
1335
  for (let attempt = 0; attempt < 5; attempt++) try {
1173
1336
  fs.renameSync(tempDir, destDir);
@@ -1184,9 +1347,16 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1184
1347
  return binaryPath;
1185
1348
  }
1186
1349
  if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
1187
- else throw renameErr;
1350
+ else {
1351
+ if (fs.existsSync(destDir) && (!fs.existsSync(binaryPath) || !fs.existsSync(completeMarker))) try {
1352
+ fs.rmSync(destDir, {
1353
+ recursive: true,
1354
+ force: true
1355
+ });
1356
+ } catch {}
1357
+ throw renameErr;
1358
+ }
1188
1359
  }
1189
- if (!isBinaryValid(binaryPath)) throw new Error(`Extracted LuaLS binary at ${binaryPath} is invalid or non-functional.`);
1190
1360
  if (!options?.quiet) console.log(`[luals] Ready: ${binaryPath}`);
1191
1361
  return binaryPath;
1192
1362
  } finally {
@@ -1200,6 +1370,11 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1200
1370
  }
1201
1371
  async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1202
1372
  if (process.env.LUALS_BIN && fs.existsSync(process.env.LUALS_BIN)) return process.env.LUALS_BIN;
1373
+ if (!version || version === "latest") {
1374
+ const defaultInfo = getPlatformInfo(FALLBACK_LUALS_VERSION);
1375
+ const defaultBundledPath = path.join(getPackageRoot(), defaultInfo.binaryRelativePath);
1376
+ if (fs.existsSync(defaultBundledPath) && isBinaryValid(defaultBundledPath)) return defaultBundledPath;
1377
+ }
1203
1378
  const resolvedVersion = await resolveLuaLSVersion(version);
1204
1379
  const info = getPlatformInfo(resolvedVersion);
1205
1380
  const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
@@ -1208,12 +1383,9 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1208
1383
  const cachedPath = path.join(cachedDir, info.binaryRelativePath);
1209
1384
  const completeMarker = path.join(cachedDir, ".complete");
1210
1385
  if (fs.existsSync(cachedPath)) {
1211
- if (isBinaryValid(cachedPath)) {
1212
- if (!fs.existsSync(completeMarker)) try {
1213
- fs.writeFileSync(completeMarker, resolvedVersion, "utf-8");
1214
- } catch {}
1215
- return cachedPath;
1216
- }
1386
+ if (fs.existsSync(completeMarker)) try {
1387
+ if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath)) return cachedPath;
1388
+ } catch {}
1217
1389
  if (!options?.quiet) console.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
1218
1390
  try {
1219
1391
  fs.rmSync(cachedDir, {
@@ -1222,6 +1394,24 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1222
1394
  });
1223
1395
  } catch {}
1224
1396
  }
1397
+ const legacyDir = getLegacyCacheDir(resolvedVersion);
1398
+ const legacyPath = path.join(legacyDir, info.binaryRelativePath);
1399
+ const legacyMarker = path.join(legacyDir, ".complete");
1400
+ if (fs.existsSync(legacyPath)) {
1401
+ let validLegacy = false;
1402
+ if (fs.existsSync(legacyMarker)) try {
1403
+ if (fs.readFileSync(legacyMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(legacyPath)) validLegacy = true;
1404
+ } catch {}
1405
+ else if (isBinaryValid(legacyPath)) validLegacy = true;
1406
+ if (validLegacy) {
1407
+ if (path.resolve(legacyDir) !== path.resolve(cachedDir)) try {
1408
+ fs.mkdirSync(path.dirname(cachedDir), { recursive: true });
1409
+ fs.cpSync(legacyDir, cachedDir, { recursive: true });
1410
+ if (fs.existsSync(cachedPath) && isBinaryValid(cachedPath)) return cachedPath;
1411
+ } catch {}
1412
+ return legacyPath;
1413
+ }
1414
+ }
1225
1415
  try {
1226
1416
  const cmd = process.platform === "win32" ? "where.exe" : "which";
1227
1417
  const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
@@ -1240,7 +1430,7 @@ async function runLuaLSCheck(targetPath, configPath, options) {
1240
1430
  checkDir = path.dirname(absoluteTarget);
1241
1431
  targetFileOnly = absoluteTarget;
1242
1432
  }
1243
- const tempOutputDir = path.join(os.tmpdir(), "nanos-lint");
1433
+ const tempOutputDir = systemPaths.temp;
1244
1434
  fs.mkdirSync(tempOutputDir, { recursive: true });
1245
1435
  const checkOutPath = path.join(tempOutputDir, `check-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
1246
1436
  const args = [
@@ -4488,6 +4678,228 @@ function useColor() {
4488
4678
  }
4489
4679
  new Command();
4490
4680
  //#endregion
4681
+ //#region src/annotations.ts
4682
+ const DOCGEN_REPO = "nanos-world/vscode-extension";
4683
+ const DOCGEN_BRANCH = "docgen-output";
4684
+ const RAW_ANNOTATIONS_URL = "https://raw.githubusercontent.com/nanos-world/vscode-extension/refs/heads/docgen-output/annotations.lua";
4685
+ const GITHUB_COMMITS_API = "https://api.github.com/repos/nanos-world/vscode-extension/commits/docgen-output";
4686
+ const ANNOTATIONS_FILENAME = "annotations.lua";
4687
+ const METADATA_FILENAME = "metadata.json";
4688
+ function getTodayDateString(d = /* @__PURE__ */ new Date()) {
4689
+ const year = d.getFullYear();
4690
+ const month = d.getMonth() + 1;
4691
+ const day = d.getDate();
4692
+ return {
4693
+ dateStr: `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`,
4694
+ dateObj: {
4695
+ year,
4696
+ month,
4697
+ day
4698
+ }
4699
+ };
4700
+ }
4701
+ function getAnnotationsCacheDir() {
4702
+ return path.join(systemPaths.cache, "annotations");
4703
+ }
4704
+ function getCachedAnnotationsFilePath() {
4705
+ return path.join(getAnnotationsCacheDir(), ANNOTATIONS_FILENAME);
4706
+ }
4707
+ function getAnnotationsMetadataFilePath() {
4708
+ return path.join(getAnnotationsCacheDir(), METADATA_FILENAME);
4709
+ }
4710
+ function readAnnotationsMetadata(cacheDir = getAnnotationsCacheDir()) {
4711
+ const metaPath = path.join(cacheDir, "metadata.json");
4712
+ if (!fs.existsSync(metaPath)) return null;
4713
+ try {
4714
+ const content = fs.readFileSync(metaPath, "utf-8");
4715
+ const parsed = JSON.parse(content);
4716
+ if (typeof parsed?.commitId === "string" && typeof parsed?.lastChecked === "string") return parsed;
4717
+ } catch {}
4718
+ return null;
4719
+ }
4720
+ async function fetchLatestCommitId() {
4721
+ try {
4722
+ const headers = { "User-Agent": "nanos-lint" };
4723
+ if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
4724
+ const res = await fetch(GITHUB_COMMITS_API, {
4725
+ headers,
4726
+ signal: AbortSignal.timeout(5e3)
4727
+ });
4728
+ if (res.ok) {
4729
+ const data = await res.json();
4730
+ if (typeof data.sha === "string" && /^[0-9a-fA-F]{7,40}$/.test(data.sha)) return data.sha;
4731
+ }
4732
+ } catch {}
4733
+ return null;
4734
+ }
4735
+ async function fetchRawAnnotationsContent() {
4736
+ const res = await fetch(RAW_ANNOTATIONS_URL, {
4737
+ headers: { "User-Agent": "nanos-lint" },
4738
+ signal: AbortSignal.timeout(15e3)
4739
+ });
4740
+ if (!res.ok) throw new Error(`Failed to download annotations.lua: ${res.status} ${res.statusText}`);
4741
+ const text = await res.text();
4742
+ if (!text || text.length < 1e3) throw new Error("Downloaded annotations.lua appears truncated or invalid");
4743
+ return text;
4744
+ }
4745
+ async function copyFileWithRetry(src, dest, maxRetries = 10) {
4746
+ for (let attempt = 0; attempt < maxRetries; attempt++) try {
4747
+ fs.copyFileSync(src, dest);
4748
+ return;
4749
+ } catch (err) {
4750
+ const code = err?.code;
4751
+ if ((code === "EBUSY" || code === "EPERM") && attempt < maxRetries - 1) {
4752
+ await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
4753
+ continue;
4754
+ }
4755
+ throw err;
4756
+ }
4757
+ }
4758
+ /**
4759
+ * Updates the metadata lastChecked date atomically without modifying annotations.lua.
4760
+ */
4761
+ function updateLastCheckedDate(commitId, cacheDir = getAnnotationsCacheDir()) {
4762
+ const { dateStr, dateObj } = getTodayDateString();
4763
+ const metadata = {
4764
+ commitId,
4765
+ lastChecked: dateStr,
4766
+ date: dateObj
4767
+ };
4768
+ fs.mkdirSync(cacheDir, { recursive: true });
4769
+ const metaPath = path.join(cacheDir, METADATA_FILENAME);
4770
+ const tempMetaPath = path.join(os.tmpdir(), `.metadata-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`);
4771
+ try {
4772
+ fs.writeFileSync(tempMetaPath, JSON.stringify(metadata, null, 2), "utf-8");
4773
+ fs.copyFileSync(tempMetaPath, metaPath);
4774
+ } finally {
4775
+ try {
4776
+ if (fs.existsSync(tempMetaPath)) fs.unlinkSync(tempMetaPath);
4777
+ } catch {}
4778
+ }
4779
+ return metadata;
4780
+ }
4781
+ /**
4782
+ * Downloads annotations.lua, writes it and the new metadata in a chained atomic transaction.
4783
+ * If any step fails, the previous cache state is reverted.
4784
+ */
4785
+ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCacheDir(), options) {
4786
+ fs.mkdirSync(cacheDir, { recursive: true });
4787
+ const finalAnnotationsPath = path.join(cacheDir, ANNOTATIONS_FILENAME);
4788
+ const finalMetaPath = path.join(cacheDir, METADATA_FILENAME);
4789
+ const tempDir = path.join(os.tmpdir(), `nanos-ann-tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
4790
+ const backupDir = path.join(os.tmpdir(), `nanos-ann-bak-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
4791
+ let hasBackup = false;
4792
+ try {
4793
+ fs.mkdirSync(tempDir, { recursive: true });
4794
+ if (!options?.quiet) {
4795
+ if (commitId && commitId !== "unknown") console.log(`[annotations] Downloading nanos world API annotations (${commitId.slice(0, 7)})...`);
4796
+ else console.log("[annotations] Downloading nanos world API annotations...");
4797
+ }
4798
+ const content = await fetchRawAnnotationsContent();
4799
+ const tempAnnotationsPath = path.join(tempDir, ANNOTATIONS_FILENAME);
4800
+ fs.writeFileSync(tempAnnotationsPath, content, "utf-8");
4801
+ const { dateStr, dateObj } = getTodayDateString();
4802
+ const metadata = {
4803
+ commitId,
4804
+ lastChecked: dateStr,
4805
+ date: dateObj
4806
+ };
4807
+ const tempMetaPath = path.join(tempDir, METADATA_FILENAME);
4808
+ fs.writeFileSync(tempMetaPath, JSON.stringify(metadata, null, 2), "utf-8");
4809
+ const existingMeta = readAnnotationsMetadata(cacheDir);
4810
+ if (existingMeta && existingMeta.commitId === commitId && fs.existsSync(finalAnnotationsPath) && fs.statSync(finalAnnotationsPath).size >= 1e3) return finalAnnotationsPath;
4811
+ const oldAnnotationsExists = fs.existsSync(finalAnnotationsPath);
4812
+ const oldMetaExists = fs.existsSync(finalMetaPath);
4813
+ if (oldAnnotationsExists || oldMetaExists) {
4814
+ fs.mkdirSync(backupDir, { recursive: true });
4815
+ if (oldAnnotationsExists) await copyFileWithRetry(finalAnnotationsPath, path.join(backupDir, ANNOTATIONS_FILENAME));
4816
+ if (oldMetaExists) await copyFileWithRetry(finalMetaPath, path.join(backupDir, METADATA_FILENAME));
4817
+ hasBackup = true;
4818
+ }
4819
+ await copyFileWithRetry(tempAnnotationsPath, finalAnnotationsPath);
4820
+ await copyFileWithRetry(tempMetaPath, finalMetaPath);
4821
+ if (!options?.quiet) {
4822
+ if (commitId && commitId !== "unknown") console.log(`[annotations] Updated annotations.lua to commit ${commitId.slice(0, 7)}.`);
4823
+ else console.log("[annotations] Updated annotations.lua.");
4824
+ }
4825
+ return finalAnnotationsPath;
4826
+ } catch (err) {
4827
+ if (hasBackup && fs.existsSync(backupDir)) try {
4828
+ const backupAnnotations = path.join(backupDir, ANNOTATIONS_FILENAME);
4829
+ const backupMeta = path.join(backupDir, METADATA_FILENAME);
4830
+ if (fs.existsSync(backupAnnotations)) await copyFileWithRetry(backupAnnotations, finalAnnotationsPath);
4831
+ if (fs.existsSync(backupMeta)) await copyFileWithRetry(backupMeta, finalMetaPath);
4832
+ } catch {}
4833
+ throw err;
4834
+ } finally {
4835
+ try {
4836
+ if (fs.existsSync(tempDir)) fs.rmSync(tempDir, {
4837
+ recursive: true,
4838
+ force: true
4839
+ });
4840
+ if (fs.existsSync(backupDir)) fs.rmSync(backupDir, {
4841
+ recursive: true,
4842
+ force: true
4843
+ });
4844
+ } catch {}
4845
+ }
4846
+ }
4847
+ /**
4848
+ * Resolves the nanos world annotations.lua definitions file path.
4849
+ * Resolution precedence:
4850
+ * 1. CLI option (`customPath`)
4851
+ * 2. Environment variable (`NANOS_ANNOTATIONS_PATH` or `NANOS_ANNOTATIONS`)
4852
+ * 3. Bundled inside package root (`annotations.lua`)
4853
+ * 4. User cache (checked once per day from upstream GitHub)
4854
+ */
4855
+ async function resolveAnnotations(options = {}) {
4856
+ if (options.customPath) {
4857
+ const resolved = path.resolve(options.customPath);
4858
+ if (!fs.existsSync(resolved)) throw new Error(`Custom annotations file not found: ${resolved}`);
4859
+ return resolved;
4860
+ }
4861
+ const envPath = process.env.NANOS_ANNOTATIONS_PATH || process.env.NANOS_ANNOTATIONS;
4862
+ if (envPath) {
4863
+ const resolved = path.resolve(envPath);
4864
+ if (!fs.existsSync(resolved)) throw new Error(`Annotations file specified in environment not found: ${resolved}`);
4865
+ return resolved;
4866
+ }
4867
+ const bundled = path.join(getPackageRoot(), "annotations.lua");
4868
+ if (fs.existsSync(bundled)) return bundled;
4869
+ const cacheDir = options.cacheDir ?? getAnnotationsCacheDir();
4870
+ const cachedAnnotationsFile = path.join(cacheDir, ANNOTATIONS_FILENAME);
4871
+ const metadata = readAnnotationsMetadata(cacheDir);
4872
+ const { dateStr } = getTodayDateString();
4873
+ if (metadata && metadata.lastChecked === dateStr && fs.existsSync(cachedAnnotationsFile)) return cachedAnnotationsFile;
4874
+ const latestCommitId = await fetchLatestCommitId();
4875
+ if (latestCommitId) {
4876
+ if (metadata && metadata.commitId === latestCommitId && fs.existsSync(cachedAnnotationsFile)) {
4877
+ updateLastCheckedDate(latestCommitId, cacheDir);
4878
+ return cachedAnnotationsFile;
4879
+ }
4880
+ return await downloadAndCacheAnnotations(latestCommitId, cacheDir, options);
4881
+ }
4882
+ if (fs.existsSync(cachedAnnotationsFile)) {
4883
+ try {
4884
+ updateLastCheckedDate(metadata?.commitId || "unknown", cacheDir);
4885
+ } catch {}
4886
+ return cachedAnnotationsFile;
4887
+ }
4888
+ try {
4889
+ return await downloadAndCacheAnnotations("unknown", cacheDir, options);
4890
+ } catch (err) {
4891
+ if (Boolean(err && typeof err === "object" && "code" in err && typeof err.code === "string" && [
4892
+ "EACCES",
4893
+ "EPERM",
4894
+ "ENOSPC",
4895
+ "EROFS",
4896
+ "EEXIST",
4897
+ "ENOENT"
4898
+ ].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 });
4899
+ 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 });
4900
+ }
4901
+ }
4902
+ //#endregion
4491
4903
  //#region src/cli.ts
4492
4904
  function getVersionString() {
4493
4905
  const root = getPackageRoot();
@@ -4513,7 +4925,7 @@ function createProgram(options) {
4513
4925
  "Warning",
4514
4926
  "Information",
4515
4927
  "Hint"
4516
- ]).default("Warning")).option("--config <path>", "Path to custom .luarc.json configuration file").addOption(new Option("--format <format>", "Output format: pretty, json, github (default: pretty, auto-detects GitHub Actions)").choices([
4928
+ ]).default("Warning")).option("--config <path>", "Path to custom .luarc.json configuration file").option("--annotations <path>", "Path to custom annotations.lua file").addOption(new Option("--format <format>", "Output format: pretty, json, github (default: pretty, auto-detects GitHub Actions)").choices([
4517
4929
  "pretty",
4518
4930
  "json",
4519
4931
  "github"
@@ -4529,7 +4941,18 @@ function createProgram(options) {
4529
4941
  quiet: opts.quiet,
4530
4942
  ignore: opts.ignore
4531
4943
  };
4532
- const resolved = resolveWorkspaceConfig(targetPath, checkOptions.configpath, { ignore: checkOptions.ignore });
4944
+ if (opts.config) {
4945
+ const resolvedConfig = path.resolve(opts.config);
4946
+ if (!fs.existsSync(resolvedConfig)) throw new Error(`Configuration file not found: ${resolvedConfig}`);
4947
+ }
4948
+ const annotationsPath = await resolveAnnotations({
4949
+ customPath: opts.annotations,
4950
+ quiet: opts.quiet
4951
+ });
4952
+ const resolved = resolveWorkspaceConfig(targetPath, checkOptions.configpath, {
4953
+ ignore: checkOptions.ignore,
4954
+ annotationsPath
4955
+ });
4533
4956
  let result;
4534
4957
  try {
4535
4958
  result = await runLuaLSCheck(targetPath, resolved.configPath, checkOptions);
@@ -4543,8 +4966,12 @@ function createProgram(options) {
4543
4966
  if (!result.passed && checkOptions.failOnError) setExitCode(1);
4544
4967
  else setExitCode(0);
4545
4968
  });
4546
- program.command("init [path]").description("Scaffold a .luarc.json configuration in the workspace").option("-f, --force", "Overwrite existing .luarc.json configuration").action((targetPath = ".", opts) => {
4547
- const created = initWorkspace(path.resolve(targetPath), { force: opts.force });
4969
+ program.command("init [path]").description("Scaffold a .luarc.json configuration in the workspace").option("-f, --force", "Overwrite existing .luarc.json configuration").option("--annotations <path>", "Path to custom annotations.lua file").action(async (targetPath = ".", opts) => {
4970
+ const annotationsPath = await resolveAnnotations({ customPath: opts.annotations });
4971
+ const created = initWorkspace(path.resolve(targetPath), {
4972
+ force: opts.force,
4973
+ annotationsPath
4974
+ });
4548
4975
  console.log(`[init] Initialized nanos world LuaLS configuration: ${created}`);
4549
4976
  setExitCode(0);
4550
4977
  });
@@ -4555,6 +4982,17 @@ function createProgram(options) {
4555
4982
  console.log(`[luals] Ready at: ${bin}`);
4556
4983
  setExitCode(0);
4557
4984
  });
4985
+ program.command("clean-cache").alias("clean").description("Clear the nanos-lint cache").action(() => {
4986
+ try {
4987
+ const cleared = cleanCache();
4988
+ if (cleared) console.log(`[cache] Cleared cache at: ${cleared}`);
4989
+ else console.log(`[cache] Cache is already empty (${systemPaths.cache})`);
4990
+ setExitCode(0);
4991
+ } catch (err) {
4992
+ console.error(`[cache] Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
4993
+ setExitCode(1);
4994
+ }
4995
+ });
4558
4996
  program.command("version").description("Show version information").action(() => {
4559
4997
  console.log(getVersionString());
4560
4998
  setExitCode(0);
@@ -4567,6 +5005,7 @@ Examples:
4567
5005
  $ npx nanos-lint check . --checklevel=Error
4568
5006
  $ npx nanos-lint check . --ignore "myfolder/hello-*.lua"
4569
5007
  $ npx nanos-lint init
5008
+ $ npx nanos-lint clean-cache
4570
5009
  `);
4571
5010
  return program;
4572
5011
  }
@@ -4620,6 +5059,6 @@ if (isDirectExecution()) runCLI().then((code) => {
4620
5059
  process.exit(1);
4621
5060
  });
4622
5061
  //#endregion
4623
- export { mergeConfigs as A, resolveLuaLSVersion as C, getPackageRoot as D, getDefinitionsDir as E, resolveWorkspaceConfig as M, stripJsonComments as N, initWorkspace as O, fileUriToPath as P, resolveLuaLSBinary as S, getDefaultTemplatePath as T, escapePowerShellSingleQuote as _, formatGitHubAnnotations as a, isBinaryValid as b, formatReport as c, pluralize as d, shouldEnableColor as f, downloadAndExtractLuaLS as g, countCheckedFiles as h, runCLI as i, parseJsonc as j, loadConfigFile as k, formatSeverityBadge as l, FALLBACK_LUALS_VERSION as m, createProgram as n, formatPretty as o, DEFAULT_LUALS_VERSION as p, isDirectExecution as r, formatProblemSummary as s, collectIgnorePatterns as t, getColors as u, getCacheDir as v, runLuaLSCheck as w, resolveLatestLuaLSVersion as x, getPlatformInfo as y };
5062
+ export { stripTrailingSlashes as $, FALLBACK_LUALS_VERSION as A, resolveLuaLSVersion as B, formatProblemSummary as C, pluralize as D, getColors as E, getLegacyCacheDir as F, getDefinitionsDir as G, sanitizeLuaLSVersion as H, getPlatformInfo as I, loadConfigFile as J, getPackageRoot as K, isBinaryValid as L, downloadAndExtractLuaLS as M, escapePowerShellSingleQuote as N, shouldEnableColor as O, getCacheDir as P, stripJsonComments as Q, resolveLatestLuaLSVersion as R, formatPretty as S, formatSeverityBadge as T, getDefaultAnnotationsPath as U, runLuaLSCheck as V, getDefaultTemplatePath as W, parseJsonc as X, mergeConfigs as Y, resolveWorkspaceConfig as Z, getTodayDateString as _, ANNOTATIONS_FILENAME as a, updateLastCheckedDate as b, GITHUB_COMMITS_API as c, downloadAndCacheAnnotations as d, cleanCache as et, fetchLatestCommitId as f, getCachedAnnotationsFilePath as g, getAnnotationsMetadataFilePath as h, runCLI as i, countCheckedFiles as j, DEFAULT_LUALS_VERSION as k, METADATA_FILENAME as l, getAnnotationsCacheDir as m, createProgram as n, fileUriToPath as nt, DOCGEN_BRANCH as o, fetchRawAnnotationsContent as p, initWorkspace as q, isDirectExecution as r, DOCGEN_REPO as s, collectIgnorePatterns as t, systemPaths as tt, RAW_ANNOTATIONS_URL as u, readAnnotationsMetadata as v, formatReport as w, formatGitHubAnnotations as x, resolveAnnotations as y, resolveLuaLSBinary as z };
4624
5063
 
4625
- //# sourceMappingURL=cli-kmHD4epr.js.map
5064
+ //# sourceMappingURL=cli-5LLXRlN7.js.map