nanos-lint 2.6.1 → 2.7.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.
@@ -6,6 +6,67 @@ import process$1 from "node:process";
6
6
  import childProcess, { execFile, execFileSync } from "node:child_process";
7
7
  import { promisify, stripVTControlCharacters } from "node:util";
8
8
  import { EventEmitter } from "node:events";
9
+ //#region src/logger.ts
10
+ const LOG_LEVELS = [
11
+ "silent",
12
+ "error",
13
+ "warn",
14
+ "info",
15
+ "debug"
16
+ ];
17
+ const LOG_LEVEL_PRIORITY = {
18
+ silent: 0,
19
+ error: 1,
20
+ warn: 2,
21
+ info: 3,
22
+ debug: 4
23
+ };
24
+ const DEFAULT_LOG_LEVEL = "warn";
25
+ function isValidLogLevel(level) {
26
+ return level in LOG_LEVEL_PRIORITY;
27
+ }
28
+ function parseLogLevel(raw) {
29
+ if (!raw) return DEFAULT_LOG_LEVEL;
30
+ const lower = raw.trim().toLowerCase();
31
+ return isValidLogLevel(lower) ? lower : DEFAULT_LOG_LEVEL;
32
+ }
33
+ var Logger = class {
34
+ level = DEFAULT_LOG_LEVEL;
35
+ constructor(initialLevel) {
36
+ if (initialLevel && isValidLogLevel(initialLevel)) this.level = initialLevel;
37
+ else {
38
+ const envLevel = process.env.NANOS_LOG_LEVEL;
39
+ if (envLevel && isValidLogLevel(envLevel.trim().toLowerCase())) this.level = envLevel.trim().toLowerCase();
40
+ }
41
+ }
42
+ setLevel(level) {
43
+ if (isValidLogLevel(level)) this.level = level;
44
+ }
45
+ getLevel() {
46
+ return this.level;
47
+ }
48
+ isEnabledFor(level) {
49
+ return LOG_LEVEL_PRIORITY[this.level] >= LOG_LEVEL_PRIORITY[level];
50
+ }
51
+ /** Whether command results should be written; only `silent` disables them. */
52
+ isOutputEnabled() {
53
+ return this.level !== "silent";
54
+ }
55
+ error(...args) {
56
+ if (this.isEnabledFor("error")) console.error(...args);
57
+ }
58
+ warn(...args) {
59
+ if (this.isEnabledFor("warn")) console.warn(...args);
60
+ }
61
+ info(...args) {
62
+ if (this.isEnabledFor("info")) console.log(...args);
63
+ }
64
+ debug(...args) {
65
+ if (this.isEnabledFor("debug")) console.debug(...args);
66
+ }
67
+ };
68
+ const logger = new Logger();
69
+ //#endregion
9
70
  //#region src/types.ts
10
71
  /**
11
72
  * 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`)
@@ -18,11 +79,14 @@ function fileUriToPath(uri) {
18
79
  if (/^\/[a-zA-Z]:/.test(res)) res = res.slice(1);
19
80
  if (/^[a-zA-Z]:/.test(res)) res = res.charAt(0).toUpperCase() + res.slice(1);
20
81
  return res;
21
- } catch {
82
+ } catch (err) {
83
+ logger.debug(`fileURLToPath fallback for URI "${uri}": ${err instanceof Error ? err.message : String(err)}`);
22
84
  let decoded = uri.slice(7);
23
85
  try {
24
86
  decoded = decodeURIComponent(decoded);
25
- } catch {}
87
+ } catch (decodeErr) {
88
+ logger.warn(`decodeURIComponent failed for path "${decoded}": ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
89
+ }
26
90
  if (decoded.startsWith("//")) return decoded;
27
91
  if (!decoded.startsWith("/") && uri.startsWith("file://") && !uri.startsWith("file:///")) return `//${decoded}`;
28
92
  if (/^\/[a-zA-Z]:/.test(decoded)) decoded = decoded.slice(1);
@@ -949,7 +1013,9 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
949
1013
  const candidate = path.join(definitionsPath, "annotations.lua");
950
1014
  if (fs.existsSync(candidate)) resolvedPath = candidate;
951
1015
  }
952
- } catch {}
1016
+ } catch (err) {
1017
+ logger.debug(`Error checking annotations definitionsPath directory (${definitionsPath}): ${err instanceof Error ? err.message : String(err)}`);
1018
+ }
953
1019
  const normalizedDefPath = resolvedPath.split(path.sep).join("/");
954
1020
  const baseLibraries = base.workspace?.library ?? [];
955
1021
  const overrideLibraries = override.workspace?.library ?? [];
@@ -1162,9 +1228,10 @@ function escapePowerShellSingleQuote(str) {
1162
1228
  return str.replace(/'/g, "''");
1163
1229
  }
1164
1230
  /**
1165
- * Resolves the latest available LuaLS release tag from the GitHub API.
1231
+ * Fetches the latest available LuaLS release tag from the GitHub API.
1232
+ * Returns null if the request fails, times out, or receives an invalid tag.
1166
1233
  */
1167
- async function resolveLatestLuaLSVersion() {
1234
+ async function fetchLatestLuaLSVersionFromGitHub() {
1168
1235
  try {
1169
1236
  const headers = { "User-Agent": "nanos-lint" };
1170
1237
  if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
@@ -1177,8 +1244,17 @@ async function resolveLatestLuaLSVersion() {
1177
1244
  const version = typeof data.tag_name === "string" ? sanitizeLuaLSVersion(data.tag_name) : null;
1178
1245
  if (version) return version;
1179
1246
  }
1180
- } catch {}
1181
- return FALLBACK_LUALS_VERSION;
1247
+ } catch (err) {
1248
+ logger.debug(`[luals] Failed to resolve latest LuaLS version from GitHub API: ${err instanceof Error ? err.message : String(err)}`);
1249
+ }
1250
+ return null;
1251
+ }
1252
+ /**
1253
+ * Resolves the latest available LuaLS release tag from the GitHub API,
1254
+ * falling back to FALLBACK_LUALS_VERSION if offline or unreachable.
1255
+ */
1256
+ async function resolveLatestLuaLSVersion() {
1257
+ return await fetchLatestLuaLSVersionFromGitHub() || "3.19.1";
1182
1258
  }
1183
1259
  /**
1184
1260
  * Resolves a version string ("latest" -> actual tag).
@@ -1226,8 +1302,15 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
1226
1302
  };
1227
1303
  throw new Error(`Unsupported platform: ${platform}`);
1228
1304
  }
1229
- function getCacheDir(version = FALLBACK_LUALS_VERSION) {
1230
- return path.join(systemPaths.cache, "luals", version);
1305
+ function getBaseLuaLSCacheDir() {
1306
+ return path.join(systemPaths.cache, "luals");
1307
+ }
1308
+ /**
1309
+ * Cache directory of a LuaLS version. `baseCacheDir` defaults to the system
1310
+ * cache and can be overridden to keep a caller isolated from it.
1311
+ */
1312
+ function getCacheDir(version = FALLBACK_LUALS_VERSION, baseCacheDir = getBaseLuaLSCacheDir()) {
1313
+ return path.join(baseCacheDir, version);
1231
1314
  }
1232
1315
  /**
1233
1316
  * Returns the legacy cache directory used in nanos-lint <= 2.2.1.
@@ -1236,6 +1319,131 @@ function getLegacyCacheDir(version = FALLBACK_LUALS_VERSION) {
1236
1319
  const base = process.platform === "win32" ? process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
1237
1320
  return path.join(base, "nanos-lint", "luals", version);
1238
1321
  }
1322
+ const LUALS_METADATA_FILENAME = "metadata.json";
1323
+ /**
1324
+ * Calculates the ISO 8601 week string for a given date in the format 'YYYY-Www' (e.g. '2026-W39').
1325
+ */
1326
+ function getIsoWeek(d = /* @__PURE__ */ new Date()) {
1327
+ const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
1328
+ const dayNum = date.getUTCDay() || 7;
1329
+ date.setUTCDate(date.getUTCDate() + 4 - dayNum);
1330
+ const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
1331
+ const weekNo = Math.ceil(((date.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
1332
+ return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
1333
+ }
1334
+ function getLuaLSMetadataPath(baseCacheDir = getBaseLuaLSCacheDir()) {
1335
+ return path.join(baseCacheDir, LUALS_METADATA_FILENAME);
1336
+ }
1337
+ function readLuaLSMetadata(baseCacheDir = getBaseLuaLSCacheDir()) {
1338
+ const metaPath = getLuaLSMetadataPath(baseCacheDir);
1339
+ if (!fs.existsSync(metaPath)) return null;
1340
+ try {
1341
+ const content = fs.readFileSync(metaPath, "utf-8");
1342
+ const parsed = JSON.parse(content);
1343
+ if (typeof parsed?.lastCheckedWeek === "string" && typeof parsed?.latestVersion === "string") return parsed;
1344
+ } catch (err) {
1345
+ logger.debug(`[luals] Failed to parse LuaLS metadata: ${err instanceof Error ? err.message : String(err)}`);
1346
+ }
1347
+ return null;
1348
+ }
1349
+ function writeLuaLSMetadata(metadata, baseCacheDir = getBaseLuaLSCacheDir()) {
1350
+ try {
1351
+ fs.mkdirSync(baseCacheDir, { recursive: true });
1352
+ const metaPath = getLuaLSMetadataPath(baseCacheDir);
1353
+ fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
1354
+ } catch (err) {
1355
+ logger.warn(`[luals] Failed to write LuaLS metadata: ${err instanceof Error ? err.message : String(err)}`);
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Lists all cached LuaLS versions in baseCacheDir that have a valid .complete marker
1360
+ * and a functional executable binary.
1361
+ */
1362
+ function listCachedLuaLSVersions(baseCacheDir = getBaseLuaLSCacheDir()) {
1363
+ if (!fs.existsSync(baseCacheDir)) return [];
1364
+ try {
1365
+ const entries = fs.readdirSync(baseCacheDir, { withFileTypes: true });
1366
+ const versions = [];
1367
+ for (const entry of entries) {
1368
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1369
+ const version = sanitizeLuaLSVersion(entry.name);
1370
+ if (!version) continue;
1371
+ const dirPath = path.join(baseCacheDir, entry.name);
1372
+ const marker = path.join(dirPath, ".complete");
1373
+ const info = getPlatformInfo(version);
1374
+ const bin = path.join(dirPath, info.binaryRelativePath);
1375
+ if (fs.existsSync(marker) && fs.existsSync(bin)) try {
1376
+ if (fs.readFileSync(marker, "utf-8").trim() === version && isBinaryValid(bin)) versions.push(version);
1377
+ } catch (err) {
1378
+ logger.debug(`[luals] Error validating cached version ${version}: ${err instanceof Error ? err.message : String(err)}`);
1379
+ }
1380
+ }
1381
+ return versions;
1382
+ } catch (err) {
1383
+ logger.debug(`[luals] Failed to list cached LuaLS versions: ${err instanceof Error ? err.message : String(err)}`);
1384
+ return [];
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Removes older cached LuaLS version directories, preserving keepVersion,
1389
+ * hidden/temporary directories, and metadata files.
1390
+ */
1391
+ function cleanupOldCachedLuaLSVersions(keepVersion, baseCacheDir = getBaseLuaLSCacheDir()) {
1392
+ if (!fs.existsSync(baseCacheDir)) return [];
1393
+ const removed = [];
1394
+ try {
1395
+ const entries = fs.readdirSync(baseCacheDir, { withFileTypes: true });
1396
+ for (const entry of entries) {
1397
+ if (!entry.isDirectory()) continue;
1398
+ if (entry.name.startsWith(".")) continue;
1399
+ if (entry.name !== keepVersion) {
1400
+ const dirPath = path.join(baseCacheDir, entry.name);
1401
+ try {
1402
+ fs.rmSync(dirPath, {
1403
+ recursive: true,
1404
+ force: true,
1405
+ maxRetries: 5,
1406
+ retryDelay: 100
1407
+ });
1408
+ removed.push(entry.name);
1409
+ logger.info(`[luals] Cleaned up older cached LuaLS version: ${entry.name}`);
1410
+ } catch (err) {
1411
+ logger.warn(`[luals] Failed to remove older cached LuaLS version at ${dirPath}: ${err instanceof Error ? err.message : String(err)}`);
1412
+ }
1413
+ }
1414
+ }
1415
+ } catch (err) {
1416
+ logger.debug(`[luals] Failed to clean up old LuaLS versions: ${err instanceof Error ? err.message : String(err)}`);
1417
+ }
1418
+ return removed;
1419
+ }
1420
+ /**
1421
+ * Locates an existing, valid LuaLS directory for the specified version in the
1422
+ * primary cache (`baseCacheDir`), the legacy cache, or the package root.
1423
+ */
1424
+ function findExistingLuaLSDir(version, baseCacheDir = getBaseLuaLSCacheDir()) {
1425
+ const info = getPlatformInfo(version);
1426
+ const primaryCache = getCacheDir(version, baseCacheDir);
1427
+ const primaryBin = path.join(primaryCache, info.binaryRelativePath);
1428
+ const primaryMarker = path.join(primaryCache, ".complete");
1429
+ if (fs.existsSync(primaryMarker)) try {
1430
+ if (fs.readFileSync(primaryMarker, "utf-8").trim() === version && isBinaryValid(primaryBin)) return primaryCache;
1431
+ } catch (err) {
1432
+ logger.debug(`[luals] Error checking primary LuaLS cache marker at ${primaryMarker}: ${err instanceof Error ? err.message : String(err)}`);
1433
+ }
1434
+ 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)}`);
1441
+ }
1442
+ const pkgRoot = getPackageRoot();
1443
+ const pkgBin = path.join(pkgRoot, info.binaryRelativePath);
1444
+ if (fs.existsSync(pkgBin) && isBinaryValid(pkgBin)) return pkgRoot;
1445
+ return null;
1446
+ }
1239
1447
  /**
1240
1448
  * Verifies that a LuaLS binary exists, has non-trivial size, and is executable.
1241
1449
  */
@@ -1250,7 +1458,8 @@ function isBinaryValid(binaryPath) {
1250
1458
  encoding: "utf-8"
1251
1459
  });
1252
1460
  return /^\d+\.\d+\.\d+/.test(output.trim());
1253
- } catch {
1461
+ } catch (err) {
1462
+ logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
1254
1463
  return false;
1255
1464
  }
1256
1465
  }
@@ -1263,72 +1472,79 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1263
1472
  if (fs.existsSync(destDir)) {
1264
1473
  if (fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
1265
1474
  if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(binaryPath)) return binaryPath;
1266
- } catch {}
1475
+ } catch (err) {
1476
+ logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
1477
+ }
1267
1478
  try {
1268
1479
  fs.rmSync(destDir, {
1269
1480
  recursive: true,
1270
1481
  force: true
1271
1482
  });
1272
- } catch {}
1483
+ } catch (err) {
1484
+ logger.warn(`[luals] Failed to remove stale or invalid cache dir ${destDir}: ${err instanceof Error ? err.message : String(err)}`);
1485
+ }
1273
1486
  }
1274
1487
  const parentDir = path.dirname(destDir);
1275
1488
  fs.mkdirSync(parentDir, { recursive: true });
1276
1489
  const tempDir = path.join(parentDir, `.${path.basename(destDir)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1277
1490
  fs.mkdirSync(tempDir, { recursive: true });
1491
+ const existingSourceDir = options?.reuseExisting !== false ? findExistingLuaLSDir(resolvedVersion, options?.cacheDir ?? getBaseLuaLSCacheDir()) : null;
1492
+ const shouldCopyFromExisting = existingSourceDir !== null && path.resolve(existingSourceDir) !== path.resolve(destDir);
1278
1493
  const url = `https://github.com/LuaLS/lua-language-server/releases/download/${resolvedVersion}/${info.assetName}`;
1279
1494
  const archivePath = path.join(tempDir, info.assetName);
1280
- if (!options?.quiet) console.log(`[luals] Downloading LuaLS ${resolvedVersion} from ${url}...`);
1281
- let response = null;
1282
- let lastErr = null;
1283
- for (let attempt = 1; attempt <= 3; attempt++) {
1284
- try {
1285
- const res = await fetch(url);
1286
- if (res.ok && res.body) {
1287
- response = res;
1288
- break;
1289
- }
1290
- await res.body?.cancel();
1291
- lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
1292
- } catch (err) {
1293
- lastErr = err;
1294
- }
1295
- if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
1296
- }
1297
- if (!response || !response.body) {
1298
- try {
1299
- fs.rmSync(tempDir, {
1300
- recursive: true,
1301
- force: true
1302
- });
1303
- } catch {}
1304
- throw lastErr || /* @__PURE__ */ new Error(`Failed to download ${url}`);
1305
- }
1306
1495
  try {
1307
- const arrayBuffer = await response.arrayBuffer();
1308
- fs.writeFileSync(archivePath, Buffer.from(arrayBuffer));
1309
- if (!options?.quiet) console.log(`[luals] Extracting to ${destDir}...`);
1310
- try {
1311
- await execFileAsync("tar", [
1312
- "-xf",
1313
- archivePath,
1314
- "-C",
1315
- tempDir
1316
- ]);
1317
- } catch (tarErr) {
1318
- if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync("powershell.exe", [
1319
- "-NoProfile",
1320
- "-Command",
1321
- `Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
1322
- ]);
1323
- else throw tarErr;
1496
+ if (shouldCopyFromExisting) {
1497
+ if (!options?.quiet) logger.info(`[luals] Reusing existing LuaLS ${resolvedVersion} installation from ${existingSourceDir}...`);
1498
+ fs.cpSync(existingSourceDir, tempDir, { recursive: true });
1499
+ } else {
1500
+ if (!options?.quiet) logger.info(`[luals] Downloading LuaLS ${resolvedVersion} from ${url}...`);
1501
+ let response = null;
1502
+ let lastErr = null;
1503
+ for (let attempt = 1; attempt <= 3; attempt++) {
1504
+ try {
1505
+ const res = await fetch(url);
1506
+ if (res.ok && res.body) {
1507
+ response = res;
1508
+ break;
1509
+ }
1510
+ await res.body?.cancel();
1511
+ lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
1512
+ } catch (err) {
1513
+ lastErr = err;
1514
+ }
1515
+ if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
1516
+ }
1517
+ if (!response || !response.body) throw lastErr || /* @__PURE__ */ new Error(`Failed to download ${url}`);
1518
+ const arrayBuffer = await response.arrayBuffer();
1519
+ fs.writeFileSync(archivePath, Buffer.from(arrayBuffer));
1520
+ if (!options?.quiet) logger.info(`[luals] Extracting to ${destDir}...`);
1521
+ try {
1522
+ await execFileAsync("tar", [
1523
+ "-xf",
1524
+ archivePath,
1525
+ "-C",
1526
+ tempDir
1527
+ ]);
1528
+ } catch (tarErr) {
1529
+ if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync("powershell.exe", [
1530
+ "-NoProfile",
1531
+ "-Command",
1532
+ `Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
1533
+ ]);
1534
+ else throw tarErr;
1535
+ }
1536
+ try {
1537
+ fs.unlinkSync(archivePath);
1538
+ } catch (err) {
1539
+ logger.debug(`[luals] Failed to delete temporary archive ${archivePath}: ${err instanceof Error ? err.message : String(err)}`);
1540
+ }
1324
1541
  }
1325
- try {
1326
- fs.unlinkSync(archivePath);
1327
- } catch {}
1328
1542
  const tempBinaryPath = path.join(tempDir, info.binaryRelativePath);
1329
1543
  if (process.platform !== "win32") try {
1330
1544
  fs.chmodSync(tempBinaryPath, 493);
1331
- } catch {}
1545
+ } catch (err) {
1546
+ logger.warn(`[luals] Failed to chmod binary at ${tempBinaryPath}: ${err instanceof Error ? err.message : String(err)}`);
1547
+ }
1332
1548
  if (!fs.existsSync(tempBinaryPath) || fs.statSync(tempBinaryPath).size < 1e5) throw new Error(`Failed to extract valid LuaLS binary to expected path: ${tempBinaryPath}`);
1333
1549
  if (!isBinaryValid(tempBinaryPath)) throw new Error(`Extracted LuaLS binary at ${tempBinaryPath} is invalid or non-functional.`);
1334
1550
  fs.writeFileSync(path.join(tempDir, ".complete"), resolvedVersion, "utf-8");
@@ -1342,8 +1558,10 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1342
1558
  recursive: true,
1343
1559
  force: true
1344
1560
  });
1345
- } catch {}
1346
- if (!options?.quiet) console.log(`[luals] Ready: ${binaryPath}`);
1561
+ } catch (err) {
1562
+ logger.debug(`[luals] Failed to remove temp directory after concurrent promotion: ${err instanceof Error ? err.message : String(err)}`);
1563
+ }
1564
+ if (!options?.quiet) logger.info(`[luals] Ready: ${binaryPath}`);
1347
1565
  return binaryPath;
1348
1566
  }
1349
1567
  if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
@@ -1353,11 +1571,13 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1353
1571
  recursive: true,
1354
1572
  force: true
1355
1573
  });
1356
- } catch {}
1574
+ } catch (err) {
1575
+ logger.warn(`[luals] Failed to clean up broken destination directory ${destDir}: ${err instanceof Error ? err.message : String(err)}`);
1576
+ }
1357
1577
  throw renameErr;
1358
1578
  }
1359
1579
  }
1360
- if (!options?.quiet) console.log(`[luals] Ready: ${binaryPath}`);
1580
+ if (!options?.quiet) logger.info(`[luals] Ready: ${binaryPath}`);
1361
1581
  return binaryPath;
1362
1582
  } finally {
1363
1583
  if (fs.existsSync(tempDir)) try {
@@ -1365,7 +1585,9 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
1365
1585
  recursive: true,
1366
1586
  force: true
1367
1587
  });
1368
- } catch {}
1588
+ } catch (err) {
1589
+ logger.debug(`[luals] Failed to clean up temporary directory ${tempDir}: ${err instanceof Error ? err.message : String(err)}`);
1590
+ }
1369
1591
  }
1370
1592
  }
1371
1593
  async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
@@ -1375,50 +1597,112 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
1375
1597
  const defaultBundledPath = path.join(getPackageRoot(), defaultInfo.binaryRelativePath);
1376
1598
  if (fs.existsSync(defaultBundledPath) && isBinaryValid(defaultBundledPath)) return defaultBundledPath;
1377
1599
  }
1378
- const resolvedVersion = await resolveLuaLSVersion(version);
1379
- const info = getPlatformInfo(resolvedVersion);
1380
- const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
1381
- if (fs.existsSync(bundledPath) && isBinaryValid(bundledPath)) return bundledPath;
1382
- const cachedDir = getCacheDir(resolvedVersion);
1383
- const cachedPath = path.join(cachedDir, info.binaryRelativePath);
1384
- const completeMarker = path.join(cachedDir, ".complete");
1385
- if (fs.existsSync(cachedPath)) {
1386
- if (fs.existsSync(completeMarker)) try {
1387
- if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath)) return cachedPath;
1388
- } catch {}
1389
- if (!options?.quiet) console.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
1600
+ const isDefaultOrLatest = !version || version === "latest";
1601
+ const baseCacheDir = options?.cacheDir ?? getBaseLuaLSCacheDir();
1602
+ if (!isDefaultOrLatest) {
1603
+ const resolvedVersion = await resolveLuaLSVersion(version);
1604
+ const info = getPlatformInfo(resolvedVersion);
1605
+ const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
1606
+ if (fs.existsSync(bundledPath) && isBinaryValid(bundledPath)) return bundledPath;
1607
+ const cachedDir = getCacheDir(resolvedVersion, baseCacheDir);
1608
+ const cachedPath = path.join(cachedDir, info.binaryRelativePath);
1609
+ const completeMarker = path.join(cachedDir, ".complete");
1610
+ if (fs.existsSync(cachedPath)) {
1611
+ if (fs.existsSync(completeMarker)) try {
1612
+ if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath)) return cachedPath;
1613
+ } catch (err) {
1614
+ logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
1615
+ }
1616
+ if (!options?.quiet) logger.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
1617
+ try {
1618
+ fs.rmSync(cachedDir, {
1619
+ recursive: true,
1620
+ force: true
1621
+ });
1622
+ } catch (err) {
1623
+ logger.warn(`[luals] Failed to remove corrupted cache directory ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
1624
+ }
1625
+ }
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)}`);
1644
+ }
1645
+ return legacyPath;
1646
+ }
1647
+ }
1390
1648
  try {
1391
- fs.rmSync(cachedDir, {
1392
- recursive: true,
1393
- force: true
1394
- });
1395
- } catch {}
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;
1649
+ const cmd = process.platform === "win32" ? "where.exe" : "which";
1650
+ const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
1651
+ const found = stdout.trim().split(/\r?\n/)[0];
1652
+ if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
1653
+ } catch (err) {
1654
+ logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
1413
1655
  }
1414
- }
1415
- try {
1656
+ return await downloadAndExtractLuaLS(resolvedVersion, getCacheDir(resolvedVersion, baseCacheDir), options);
1657
+ }
1658
+ const currentWeek = getIsoWeek();
1659
+ const metadata = readLuaLSMetadata(baseCacheDir);
1660
+ if (metadata && metadata.lastCheckedWeek === currentWeek && metadata.latestVersion) {
1661
+ const info = getPlatformInfo(metadata.latestVersion);
1662
+ const cachedPath = path.join(getCacheDir(metadata.latestVersion, baseCacheDir), info.binaryRelativePath);
1663
+ if (isBinaryValid(cachedPath)) return cachedPath;
1664
+ }
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);
1670
+ }
1671
+ const onlineTag = await fetchLatestLuaLSVersionFromGitHub();
1672
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1673
+ let targetVersion;
1674
+ if (onlineTag) targetVersion = onlineTag;
1675
+ else if (metadata?.latestVersion && cachedVersions.includes(metadata.latestVersion)) {
1676
+ logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${metadata.latestVersion}.`);
1677
+ 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];
1681
+ } else targetVersion = FALLBACK_LUALS_VERSION;
1682
+ writeLuaLSMetadata({
1683
+ lastCheckedWeek: currentWeek,
1684
+ latestVersion: targetVersion,
1685
+ lastCheckedDate: today
1686
+ }, baseCacheDir);
1687
+ const info = getPlatformInfo(targetVersion);
1688
+ const targetCacheDir = getCacheDir(targetVersion, baseCacheDir);
1689
+ const targetBinaryPath = path.join(targetCacheDir, info.binaryRelativePath);
1690
+ const completeMarker = path.join(targetCacheDir, ".complete");
1691
+ if (fs.existsSync(completeMarker) && isBinaryValid(targetBinaryPath)) {
1692
+ cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
1693
+ return targetBinaryPath;
1694
+ }
1695
+ if (!onlineTag) try {
1416
1696
  const cmd = process.platform === "win32" ? "where.exe" : "which";
1417
1697
  const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
1418
1698
  const found = stdout.trim().split(/\r?\n/)[0];
1419
1699
  if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
1420
- } catch {}
1421
- return await downloadAndExtractLuaLS(resolvedVersion, void 0, options);
1700
+ } catch (err) {
1701
+ logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
1702
+ }
1703
+ const downloadedBinary = await downloadAndExtractLuaLS(targetVersion, targetCacheDir, options);
1704
+ cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
1705
+ return downloadedBinary;
1422
1706
  }
1423
1707
  async function runLuaLSCheck(targetPath, configPath, options) {
1424
1708
  const absoluteTarget = path.resolve(targetPath);
@@ -1448,6 +1732,7 @@ async function runLuaLSCheck(targetPath, configPath, options) {
1448
1732
  });
1449
1733
  } catch (err) {
1450
1734
  execError = err;
1735
+ logger.debug(`[luals] LuaLS process exited with error or non-zero status: ${err instanceof Error ? err.message : String(err)}`);
1451
1736
  }
1452
1737
  let diagnostics = {};
1453
1738
  let parseSucceeded = false;
@@ -1455,10 +1740,14 @@ async function runLuaLSCheck(targetPath, configPath, options) {
1455
1740
  const content = fs.readFileSync(checkOutPath, "utf-8");
1456
1741
  diagnostics = JSON.parse(content);
1457
1742
  parseSucceeded = true;
1458
- } catch {} finally {
1743
+ } catch (err) {
1744
+ logger.error(`[luals] Failed to read or parse diagnostic output from ${checkOutPath}: ${err instanceof Error ? err.message : String(err)}`);
1745
+ } finally {
1459
1746
  try {
1460
1747
  fs.unlinkSync(checkOutPath);
1461
- } catch {}
1748
+ } catch (err) {
1749
+ logger.debug(`[luals] Failed to delete check output file ${checkOutPath}: ${err instanceof Error ? err.message : String(err)}`);
1750
+ }
1462
1751
  }
1463
1752
  if (!parseSucceeded) {
1464
1753
  const cacheHint = `(Cache location: ${getCacheDir()})`;
@@ -1514,7 +1803,9 @@ function countCheckedFiles(targetPath, configPath) {
1514
1803
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1515
1804
  if (cfg.workspace?.ignoreDir) ignoreDirs = cfg.workspace.ignoreDir;
1516
1805
  if (cfg.files?.exclude) excludePatterns = cfg.files.exclude;
1517
- } catch {}
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
+ }
1518
1809
  const normIgnoreDirs = new Set(ignoreDirs.map((d) => d.replace(/\\/g, "/").toLowerCase()));
1519
1810
  function isExcluded(relPath) {
1520
1811
  const norm = relPath.replace(/\\/g, "/");
@@ -1532,7 +1823,9 @@ function countCheckedFiles(targetPath, configPath) {
1532
1823
  const baseRegexStr = "^" + normPat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
1533
1824
  try {
1534
1825
  if (new RegExp(baseRegexStr, "i").test(baseName)) return true;
1535
- } catch {}
1826
+ } catch (err) {
1827
+ logger.debug(`[luals] Invalid regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
1828
+ }
1536
1829
  }
1537
1830
  let regexStr = normPat;
1538
1831
  const hasLeadingDoubleStar = regexStr.startsWith("**/");
@@ -1544,7 +1837,9 @@ function countCheckedFiles(targetPath, configPath) {
1544
1837
  if (hasTrailingDoubleStar) escaped = `${escaped}(?:/.*)?`;
1545
1838
  try {
1546
1839
  if (new RegExp(`^${escaped}$`, "i").test(norm)) return true;
1547
- } catch {}
1840
+ } catch (err) {
1841
+ logger.debug(`[luals] Invalid glob regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
1842
+ }
1548
1843
  }
1549
1844
  }
1550
1845
  return false;
@@ -1554,7 +1849,8 @@ function countCheckedFiles(targetPath, configPath) {
1554
1849
  let entries;
1555
1850
  try {
1556
1851
  entries = fs.readdirSync(currentDir, { withFileTypes: true });
1557
- } catch {
1852
+ } catch (err) {
1853
+ logger.debug(`[luals] Failed to read directory ${currentDir}: ${err instanceof Error ? err.message : String(err)}`);
1558
1854
  return;
1559
1855
  }
1560
1856
  for (const entry of entries) {
@@ -1564,7 +1860,8 @@ function countCheckedFiles(targetPath, configPath) {
1564
1860
  if (entry.isDirectory() || entry.isSymbolicLink() && (() => {
1565
1861
  try {
1566
1862
  return fs.statSync(fullPath).isDirectory();
1567
- } catch {
1863
+ } catch (err) {
1864
+ logger.debug(`[luals] Failed to stat symlink target ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
1568
1865
  return false;
1569
1866
  }
1570
1867
  })()) {
@@ -1661,7 +1958,9 @@ function formatPretty(result, cwd = process.cwd(), useColor = shouldEnableColor(
1661
1958
  let fileContent = [];
1662
1959
  try {
1663
1960
  if (fs.existsSync(filePath)) fileContent = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
1664
- } catch {}
1961
+ } catch (err) {
1962
+ logger.debug(`Failed to read file snippet for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
1963
+ }
1665
1964
  for (const d of diags) {
1666
1965
  const line = d.range.start.line + 1;
1667
1966
  const col = d.range.start.character + 1;
@@ -4714,7 +5013,9 @@ function readAnnotationsMetadata(cacheDir = getAnnotationsCacheDir()) {
4714
5013
  const content = fs.readFileSync(metaPath, "utf-8");
4715
5014
  const parsed = JSON.parse(content);
4716
5015
  if (typeof parsed?.commitId === "string" && typeof parsed?.lastChecked === "string") return parsed;
4717
- } catch {}
5016
+ } catch (err) {
5017
+ logger.debug(`Failed to parse annotations metadata: ${err instanceof Error ? err.message : String(err)}`);
5018
+ }
4718
5019
  return null;
4719
5020
  }
4720
5021
  async function fetchLatestCommitId() {
@@ -4729,7 +5030,9 @@ async function fetchLatestCommitId() {
4729
5030
  const data = await res.json();
4730
5031
  if (typeof data.sha === "string" && /^[0-9a-fA-F]{7,40}$/.test(data.sha)) return data.sha;
4731
5032
  }
4732
- } catch {}
5033
+ } catch (err) {
5034
+ logger.warn(`Failed to resolve latest annotations commit from GitHub: ${err instanceof Error ? err.message : String(err)}`);
5035
+ }
4733
5036
  return null;
4734
5037
  }
4735
5038
  async function fetchRawAnnotationsContent() {
@@ -4774,7 +5077,9 @@ function updateLastCheckedDate(commitId, cacheDir = getAnnotationsCacheDir()) {
4774
5077
  } finally {
4775
5078
  try {
4776
5079
  if (fs.existsSync(tempMetaPath)) fs.unlinkSync(tempMetaPath);
4777
- } catch {}
5080
+ } catch (err) {
5081
+ logger.warn(`Failed to clean up temporary metadata file ${tempMetaPath}: ${err instanceof Error ? err.message : String(err)}`);
5082
+ }
4778
5083
  }
4779
5084
  return metadata;
4780
5085
  }
@@ -4792,8 +5097,8 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
4792
5097
  try {
4793
5098
  fs.mkdirSync(tempDir, { recursive: true });
4794
5099
  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...");
5100
+ if (commitId && commitId !== "unknown") logger.info(`[annotations] Downloading nanos world API annotations (${commitId.slice(0, 7)})...`);
5101
+ else logger.info("[annotations] Downloading nanos world API annotations...");
4797
5102
  }
4798
5103
  const content = await fetchRawAnnotationsContent();
4799
5104
  const tempAnnotationsPath = path.join(tempDir, ANNOTATIONS_FILENAME);
@@ -4819,8 +5124,8 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
4819
5124
  await copyFileWithRetry(tempAnnotationsPath, finalAnnotationsPath);
4820
5125
  await copyFileWithRetry(tempMetaPath, finalMetaPath);
4821
5126
  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.");
5127
+ if (commitId && commitId !== "unknown") logger.info(`[annotations] Updated annotations.lua to commit ${commitId.slice(0, 7)}.`);
5128
+ else logger.info("[annotations] Updated annotations.lua.");
4824
5129
  }
4825
5130
  return finalAnnotationsPath;
4826
5131
  } catch (err) {
@@ -4829,7 +5134,9 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
4829
5134
  const backupMeta = path.join(backupDir, METADATA_FILENAME);
4830
5135
  if (fs.existsSync(backupAnnotations)) await copyFileWithRetry(backupAnnotations, finalAnnotationsPath);
4831
5136
  if (fs.existsSync(backupMeta)) await copyFileWithRetry(backupMeta, finalMetaPath);
4832
- } catch {}
5137
+ } catch (rollbackErr) {
5138
+ logger.error(`Failed to restore annotations from backup during rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`);
5139
+ }
4833
5140
  throw err;
4834
5141
  } finally {
4835
5142
  try {
@@ -4841,7 +5148,9 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
4841
5148
  recursive: true,
4842
5149
  force: true
4843
5150
  });
4844
- } catch {}
5151
+ } catch (err) {
5152
+ logger.warn(`Failed to clean up annotations temporary or backup directory: ${err instanceof Error ? err.message : String(err)}`);
5153
+ }
4845
5154
  }
4846
5155
  }
4847
5156
  /**
@@ -4882,7 +5191,9 @@ async function resolveAnnotations(options = {}) {
4882
5191
  if (fs.existsSync(cachedAnnotationsFile)) {
4883
5192
  try {
4884
5193
  updateLastCheckedDate(metadata?.commitId || "unknown", cacheDir);
4885
- } catch {}
5194
+ } catch (err) {
5195
+ logger.warn(`Failed to update lastChecked date for cached annotations: ${err instanceof Error ? err.message : String(err)}`);
5196
+ }
4886
5197
  return cachedAnnotationsFile;
4887
5198
  }
4888
5199
  try {
@@ -4905,10 +5216,15 @@ function getVersionString() {
4905
5216
  const root = getPackageRoot();
4906
5217
  try {
4907
5218
  return `nanos-lint v${JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8")).version}`;
4908
- } catch {
5219
+ } catch (err) {
5220
+ logger.debug(`Could not read version from package.json: ${err instanceof Error ? err.message : String(err)}`);
4909
5221
  return "nanos-lint v1.0.0";
4910
5222
  }
4911
5223
  }
5224
+ /** Writes command results to stdout unless the level is `silent`. */
5225
+ function writeOutput(message) {
5226
+ if (logger.isOutputEnabled()) console.log(message);
5227
+ }
4912
5228
  function collectIgnorePatterns(val, prev) {
4913
5229
  const parts = val.split(/[\r\n,]+/).map((s) => s.trim()).filter(Boolean);
4914
5230
  return (prev ?? []).concat(parts);
@@ -4916,9 +5232,20 @@ function collectIgnorePatterns(val, prev) {
4916
5232
  function createProgram(options) {
4917
5233
  const setExitCode = options?.setExitCode ?? (() => {});
4918
5234
  const program = new Command("nanos-lint");
4919
- program.description("Linter and type-checker for nanos world Lua scripts").version(getVersionString(), "-v, --version", "Show version information").exitOverride().configureOutput({
5235
+ program.description("Linter and type-checker for nanos world Lua scripts").version(getVersionString(), "-v, --version", "Show version information").addOption(new Option("-l, --log-level <level>", "Logging level: error, warn, info, debug, silent").choices([
5236
+ "error",
5237
+ "warn",
5238
+ "info",
5239
+ "debug",
5240
+ "silent"
5241
+ ]).default(DEFAULT_LOG_LEVEL)).hook("preAction", (thisCommand, actionCommand) => {
5242
+ const target = actionCommand || thisCommand;
5243
+ const opts = target.optsWithGlobals ? target.optsWithGlobals() : target.opts();
5244
+ if (opts.quiet) logger.setLevel("error");
5245
+ else if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
5246
+ }).exitOverride().configureOutput({
4920
5247
  writeOut: (str) => console.log(str.trimEnd()),
4921
- writeErr: (str) => console.error(str.trimEnd())
5248
+ writeErr: (str) => logger.error(str.trimEnd())
4922
5249
  });
4923
5250
  program.command("check [path]", { isDefault: true }).description("Check a workspace or Lua file (default command)").addOption(new Option("--checklevel <level>", "Minimum diagnostic level: Error, Warning, Information, Hint").choices([
4924
5251
  "Error",
@@ -4929,7 +5256,15 @@ function createProgram(options) {
4929
5256
  "pretty",
4930
5257
  "json",
4931
5258
  "github"
4932
- ])).option("-i, --ignore <pattern>", "Files or directories to ignore (supports glob patterns, repeatable)", collectIgnorePatterns).option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`, DEFAULT_LUALS_VERSION).option("--no-fail", "Do not exit with code 1 if diagnostics are found").option("--quiet", "Suppress progress output").option("--github", "Output in GitHub Actions format (shortcut for --format=github)").action(async (targetPath = ".", opts) => {
5259
+ ])).option("-i, --ignore <pattern>", "Files or directories to ignore (supports glob patterns, repeatable)", collectIgnorePatterns).option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`, DEFAULT_LUALS_VERSION).option("--no-fail", "Do not exit with code 1 if diagnostics are found").option("--quiet", "Suppress progress output").addOption(new Option("-l, --log-level <level>", "Logging level: error, warn, info, debug, silent").choices([
5260
+ "error",
5261
+ "warn",
5262
+ "info",
5263
+ "debug",
5264
+ "silent"
5265
+ ])).option("--github", "Output in GitHub Actions format (shortcut for --format=github)").action(async (targetPath = ".", opts) => {
5266
+ if (opts.quiet) logger.setLevel("error");
5267
+ else if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
4933
5268
  const format = opts.github ? "github" : opts.format || (process.env.GITHUB_ACTIONS ? "github" : "pretty");
4934
5269
  const checkOptions = {
4935
5270
  path: targetPath,
@@ -4959,42 +5294,42 @@ function createProgram(options) {
4959
5294
  } finally {
4960
5295
  if (resolved.isTemp && fs.existsSync(resolved.configPath)) try {
4961
5296
  fs.unlinkSync(resolved.configPath);
4962
- } catch {}
5297
+ } catch (err) {
5298
+ logger.warn(`Failed to clean up temporary config file ${resolved.configPath}: ${err instanceof Error ? err.message : String(err)}`);
5299
+ }
4963
5300
  }
4964
5301
  const output = formatReport(result, checkOptions.format, process.cwd());
4965
- if (output) console.log(output);
5302
+ if (output) writeOutput(output);
4966
5303
  if (!result.passed && checkOptions.failOnError) setExitCode(1);
4967
5304
  else setExitCode(0);
4968
5305
  });
4969
5306
  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
5307
  const annotationsPath = await resolveAnnotations({ customPath: opts.annotations });
4971
- const created = initWorkspace(path.resolve(targetPath), {
5308
+ writeOutput(`[init] Initialized nanos world LuaLS configuration: ${initWorkspace(path.resolve(targetPath), {
4972
5309
  force: opts.force,
4973
5310
  annotationsPath
4974
- });
4975
- console.log(`[init] Initialized nanos world LuaLS configuration: ${created}`);
5311
+ })}`);
4976
5312
  setExitCode(0);
4977
5313
  });
4978
5314
  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) => {
4979
5315
  const ver = version || opts?.lualsVersion || "latest";
4980
- console.log(`[luals] Downloading LuaLS ${ver}...`);
4981
- const bin = await resolveLuaLSBinary(ver);
4982
- console.log(`[luals] Ready at: ${bin}`);
5316
+ writeOutput(`[luals] Downloading LuaLS ${ver}...`);
5317
+ writeOutput(`[luals] Ready at: ${await resolveLuaLSBinary(ver)}`);
4983
5318
  setExitCode(0);
4984
5319
  });
4985
5320
  program.command("clean-cache").alias("clean").description("Clear the nanos-lint cache").action(() => {
4986
5321
  try {
4987
5322
  const cleared = cleanCache();
4988
- if (cleared) console.log(`[cache] Cleared cache at: ${cleared}`);
4989
- else console.log(`[cache] Cache is already empty (${systemPaths.cache})`);
5323
+ if (cleared) writeOutput(`[cache] Cleared cache at: ${cleared}`);
5324
+ else writeOutput(`[cache] Cache is already empty (${systemPaths.cache})`);
4990
5325
  setExitCode(0);
4991
5326
  } catch (err) {
4992
- console.error(`[cache] Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
5327
+ logger.error(`[cache] Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
4993
5328
  setExitCode(1);
4994
5329
  }
4995
5330
  });
4996
5331
  program.command("version").description("Show version information").action(() => {
4997
- console.log(getVersionString());
5332
+ writeOutput(getVersionString());
4998
5333
  setExitCode(0);
4999
5334
  });
5000
5335
  program.helpCommand("help [command]", "Show this help message");
@@ -5010,6 +5345,15 @@ Examples:
5010
5345
  return program;
5011
5346
  }
5012
5347
  async function runCLI(args = process.argv.slice(2)) {
5348
+ for (let i = 0; i < args.length; i++) {
5349
+ const arg = args[i];
5350
+ if (arg === "-l" || arg === "--log-level") {
5351
+ if (i + 1 < args.length && isValidLogLevel(args[i + 1])) logger.setLevel(args[i + 1]);
5352
+ } else if (arg.startsWith("--log-level=")) {
5353
+ const val = arg.split("=")[1];
5354
+ if (val && isValidLogLevel(val)) logger.setLevel(val);
5355
+ }
5356
+ }
5013
5357
  let exitCode = 0;
5014
5358
  const program = createProgram({ setExitCode: (code) => {
5015
5359
  exitCode = code;
@@ -5020,8 +5364,8 @@ async function runCLI(args = process.argv.slice(2)) {
5020
5364
  } catch (err) {
5021
5365
  if (err instanceof CommanderError) return err.exitCode;
5022
5366
  const message = err instanceof Error ? err.message : String(err);
5023
- console.error(`error: ${message}`);
5024
- if (process.env.DEBUG && err instanceof Error && err.stack) console.error(err.stack);
5367
+ logger.error(`error: ${message}`);
5368
+ if (process.env.DEBUG && err instanceof Error && err.stack) logger.error(err.stack);
5025
5369
  return 1;
5026
5370
  }
5027
5371
  }
@@ -5031,7 +5375,8 @@ function isDirectExecution(importMetaUrl = import.meta.url, argv1 = process.argv
5031
5375
  try {
5032
5376
  if (urlStr.startsWith("file:")) return fileURLToPath(urlStr);
5033
5377
  return urlStr;
5034
- } catch {
5378
+ } catch (err) {
5379
+ logger.debug(`[cli] Failed to convert URL "${urlStr}" using fileURLToPath: ${err instanceof Error ? err.message : String(err)}`);
5035
5380
  return urlStr.replace(/^file:\/\/\/?/, "");
5036
5381
  }
5037
5382
  };
@@ -5043,7 +5388,8 @@ function isDirectExecution(importMetaUrl = import.meta.url, argv1 = process.argv
5043
5388
  const cliJsPath = path.join(moduleDir, "cli.js").toLowerCase();
5044
5389
  const cliTsPath = path.join(moduleDir, "cli.ts").toLowerCase();
5045
5390
  if (scriptPath === cliJsPath || scriptPath === cliTsPath) return true;
5046
- } catch {
5391
+ } catch (err) {
5392
+ logger.debug(`[cli] Failed to resolve realpath for entry detection: ${err instanceof Error ? err.message : String(err)}`);
5047
5393
  const normArgv = path.resolve(argv1).toLowerCase();
5048
5394
  const normMeta = toPath(importMetaUrl).toLowerCase();
5049
5395
  if (normArgv === normMeta) return true;
@@ -5055,10 +5401,10 @@ function isDirectExecution(importMetaUrl = import.meta.url, argv1 = process.argv
5055
5401
  if (isDirectExecution()) runCLI().then((code) => {
5056
5402
  process.exit(code);
5057
5403
  }).catch((err) => {
5058
- console.error(err);
5404
+ logger.error(err);
5059
5405
  process.exit(1);
5060
5406
  });
5061
5407
  //#endregion
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 };
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 };
5063
5409
 
5064
- //# sourceMappingURL=cli-5LLXRlN7.js.map
5410
+ //# sourceMappingURL=cli-sEO_MZ-K.js.map