nanos-lint 2.5.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();
@@ -837,15 +934,27 @@ function stripTrailingSlashes(value) {
837
934
  }
838
935
  /**
839
936
  * Merges a base nanos configuration with a workspace override configuration.
840
- * Guarantees that the nanos definitions directory is included in workspace.library,
937
+ * Guarantees that nanos API annotations are included in workspace.library,
841
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
842
944
  */
843
- function mergeConfigs(base, override = {}, definitionsDir = getDefinitionsDir(), options) {
844
- 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("/");
845
954
  const baseLibraries = base.workspace?.library ?? [];
846
955
  const overrideLibraries = override.workspace?.library ?? [];
847
956
  const librarySet = /* @__PURE__ */ new Set([
848
- normalizedDefDir,
957
+ normalizedDefPath,
849
958
  ...baseLibraries,
850
959
  ...overrideLibraries
851
960
  ]);
@@ -938,7 +1047,7 @@ function mergeConfigs(base, override = {}, definitionsDir = getDefinitionsDir(),
938
1047
  */
939
1048
  function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
940
1049
  const defaultTemplate = loadConfigFile(getDefaultTemplatePath());
941
- const definitionsDir = getDefinitionsDir();
1050
+ const activeAnnotationsPath = options?.annotationsPath || getDefaultAnnotationsPath();
942
1051
  let userConfig = {};
943
1052
  if (customConfigPath) userConfig = loadConfigFile(path.resolve(customConfigPath));
944
1053
  else {
@@ -961,7 +1070,7 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
961
1070
  }
962
1071
  cliIgnore = expanded;
963
1072
  }
964
- const merged = mergeConfigs(defaultTemplate, userConfig, definitionsDir, { cliIgnore });
1073
+ const merged = mergeConfigs(defaultTemplate, userConfig, activeAnnotationsPath, { cliIgnore });
965
1074
  if (!hasCliIgnore) {
966
1075
  const resolvedTarget = path.resolve(workspacePath);
967
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")))) {
@@ -976,7 +1085,7 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
976
1085
  ])];
977
1086
  }
978
1087
  }
979
- const tempDir = path.join(os.tmpdir(), "nanos-lint");
1088
+ const tempDir = systemPaths.temp;
980
1089
  fs.mkdirSync(tempDir, { recursive: true });
981
1090
  const tempConfigFile = path.join(tempDir, `luarc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
982
1091
  fs.writeFileSync(tempConfigFile, JSON.stringify(merged, null, 2), "utf-8");
@@ -992,9 +1101,8 @@ function initWorkspace(workspacePath, options) {
992
1101
  const targetFile = path.join(workspacePath, ".luarc.json");
993
1102
  if (fs.existsSync(targetFile) && !options?.force) throw new Error(`.luarc.json already exists at ${targetFile}. Use --force to overwrite.`);
994
1103
  const template = loadConfigFile(getDefaultTemplatePath());
995
- const definitionsDir = getDefinitionsDir();
996
- const sourceAnnotations = path.join(definitionsDir, "annotations.lua");
997
- 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.`);
998
1106
  const targetNanosDir = path.join(workspacePath, ".nanos-lint");
999
1107
  fs.mkdirSync(targetNanosDir, { recursive: true });
1000
1108
  const targetAnnotations = path.join(targetNanosDir, "annotations.lua");
@@ -1119,6 +1227,12 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
1119
1227
  throw new Error(`Unsupported platform: ${platform}`);
1120
1228
  }
1121
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) {
1122
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");
1123
1237
  return path.join(base, "nanos-lint", "luals", version);
1124
1238
  }
@@ -1147,12 +1261,9 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1147
1261
  const binaryPath = path.join(destDir, info.binaryRelativePath);
1148
1262
  const completeMarker = path.join(destDir, ".complete");
1149
1263
  if (fs.existsSync(destDir)) {
1150
- if (fs.existsSync(binaryPath) && isBinaryValid(binaryPath)) {
1151
- if (!fs.existsSync(completeMarker)) try {
1152
- fs.writeFileSync(completeMarker, resolvedVersion, "utf-8");
1153
- } catch {}
1154
- return binaryPath;
1155
- }
1264
+ if (fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
1265
+ if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(binaryPath)) return binaryPath;
1266
+ } catch {}
1156
1267
  try {
1157
1268
  fs.rmSync(destDir, {
1158
1269
  recursive: true,
@@ -1219,6 +1330,7 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1219
1330
  fs.chmodSync(tempBinaryPath, 493);
1220
1331
  } catch {}
1221
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.`);
1222
1334
  fs.writeFileSync(path.join(tempDir, ".complete"), resolvedVersion, "utf-8");
1223
1335
  for (let attempt = 0; attempt < 5; attempt++) try {
1224
1336
  fs.renameSync(tempDir, destDir);
@@ -1235,9 +1347,16 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1235
1347
  return binaryPath;
1236
1348
  }
1237
1349
  if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
1238
- 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
+ }
1239
1359
  }
1240
- if (!isBinaryValid(binaryPath)) throw new Error(`Extracted LuaLS binary at ${binaryPath} is invalid or non-functional.`);
1241
1360
  if (!options?.quiet) console.log(`[luals] Ready: ${binaryPath}`);
1242
1361
  return binaryPath;
1243
1362
  } finally {
@@ -1251,6 +1370,11 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1251
1370
  }
1252
1371
  async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1253
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
+ }
1254
1378
  const resolvedVersion = await resolveLuaLSVersion(version);
1255
1379
  const info = getPlatformInfo(resolvedVersion);
1256
1380
  const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
@@ -1259,12 +1383,9 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1259
1383
  const cachedPath = path.join(cachedDir, info.binaryRelativePath);
1260
1384
  const completeMarker = path.join(cachedDir, ".complete");
1261
1385
  if (fs.existsSync(cachedPath)) {
1262
- if (isBinaryValid(cachedPath)) {
1263
- if (!fs.existsSync(completeMarker)) try {
1264
- fs.writeFileSync(completeMarker, resolvedVersion, "utf-8");
1265
- } catch {}
1266
- return cachedPath;
1267
- }
1386
+ if (fs.existsSync(completeMarker)) try {
1387
+ if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath)) return cachedPath;
1388
+ } catch {}
1268
1389
  if (!options?.quiet) console.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
1269
1390
  try {
1270
1391
  fs.rmSync(cachedDir, {
@@ -1273,6 +1394,24 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1273
1394
  });
1274
1395
  } catch {}
1275
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
+ }
1276
1415
  try {
1277
1416
  const cmd = process.platform === "win32" ? "where.exe" : "which";
1278
1417
  const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
@@ -1291,7 +1430,7 @@ async function runLuaLSCheck(targetPath, configPath, options) {
1291
1430
  checkDir = path.dirname(absoluteTarget);
1292
1431
  targetFileOnly = absoluteTarget;
1293
1432
  }
1294
- const tempOutputDir = path.join(os.tmpdir(), "nanos-lint");
1433
+ const tempOutputDir = systemPaths.temp;
1295
1434
  fs.mkdirSync(tempOutputDir, { recursive: true });
1296
1435
  const checkOutPath = path.join(tempOutputDir, `check-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
1297
1436
  const args = [
@@ -4539,6 +4678,228 @@ function useColor() {
4539
4678
  }
4540
4679
  new Command();
4541
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
4542
4903
  //#region src/cli.ts
4543
4904
  function getVersionString() {
4544
4905
  const root = getPackageRoot();
@@ -4564,7 +4925,7 @@ function createProgram(options) {
4564
4925
  "Warning",
4565
4926
  "Information",
4566
4927
  "Hint"
4567
- ]).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([
4568
4929
  "pretty",
4569
4930
  "json",
4570
4931
  "github"
@@ -4580,7 +4941,18 @@ function createProgram(options) {
4580
4941
  quiet: opts.quiet,
4581
4942
  ignore: opts.ignore
4582
4943
  };
4583
- 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
+ });
4584
4956
  let result;
4585
4957
  try {
4586
4958
  result = await runLuaLSCheck(targetPath, resolved.configPath, checkOptions);
@@ -4594,8 +4966,12 @@ function createProgram(options) {
4594
4966
  if (!result.passed && checkOptions.failOnError) setExitCode(1);
4595
4967
  else setExitCode(0);
4596
4968
  });
4597
- program.command("init [path]").description("Scaffold a .luarc.json configuration in the workspace").option("-f, --force", "Overwrite existing .luarc.json configuration").action((targetPath = ".", opts) => {
4598
- 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
+ });
4599
4975
  console.log(`[init] Initialized nanos world LuaLS configuration: ${created}`);
4600
4976
  setExitCode(0);
4601
4977
  });
@@ -4606,6 +4982,17 @@ function createProgram(options) {
4606
4982
  console.log(`[luals] Ready at: ${bin}`);
4607
4983
  setExitCode(0);
4608
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
+ });
4609
4996
  program.command("version").description("Show version information").action(() => {
4610
4997
  console.log(getVersionString());
4611
4998
  setExitCode(0);
@@ -4618,6 +5005,7 @@ Examples:
4618
5005
  $ npx nanos-lint check . --checklevel=Error
4619
5006
  $ npx nanos-lint check . --ignore "myfolder/hello-*.lua"
4620
5007
  $ npx nanos-lint init
5008
+ $ npx nanos-lint clean-cache
4621
5009
  `);
4622
5010
  return program;
4623
5011
  }
@@ -4671,6 +5059,6 @@ if (isDirectExecution()) runCLI().then((code) => {
4671
5059
  process.exit(1);
4672
5060
  });
4673
5061
  //#endregion
4674
- export { loadConfigFile as A, resolveLuaLSVersion as C, getDefinitionsDir as D, getDefaultTemplatePath as E, stripTrailingSlashes as F, fileUriToPath as I, parseJsonc as M, resolveWorkspaceConfig as N, getPackageRoot as O, stripJsonComments as P, resolveLuaLSBinary as S, sanitizeLuaLSVersion 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, mergeConfigs as j, initWorkspace 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 };
4675
5063
 
4676
- //# sourceMappingURL=cli-BVXk8XmH.js.map
5064
+ //# sourceMappingURL=cli-5LLXRlN7.js.map