nanos-lint 2.6.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -9
- package/action.yml +15 -6
- package/dist/{cli-5LLXRlN7.js → cli-DGez8MSI.js} +1056 -270
- package/dist/cli-DGez8MSI.js.map +1 -0
- package/dist/cli-r8LDolDk.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +172 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/templates/.luarc.json +15 -21
- package/dist/cli-5LLXRlN7.js.map +0 -1
|
@@ -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);
|
|
@@ -865,6 +929,77 @@ function cleanCache(targetDir = systemPaths.cache) {
|
|
|
865
929
|
}
|
|
866
930
|
return null;
|
|
867
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Recursively calculates total disk usage of a file or directory in bytes.
|
|
934
|
+
* Handles missing paths and inaccessible files gracefully.
|
|
935
|
+
*/
|
|
936
|
+
function getDirectorySize(targetPath) {
|
|
937
|
+
if (!fs.existsSync(targetPath)) return 0;
|
|
938
|
+
let total = 0;
|
|
939
|
+
try {
|
|
940
|
+
const stat = fs.statSync(targetPath);
|
|
941
|
+
if (!stat.isDirectory()) return stat.size;
|
|
942
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
943
|
+
for (const entry of entries) {
|
|
944
|
+
const fullPath = `${targetPath}/${entry.name}`;
|
|
945
|
+
try {
|
|
946
|
+
if (entry.isDirectory()) total += getDirectorySize(fullPath);
|
|
947
|
+
else if (entry.isFile()) total += fs.statSync(fullPath).size;
|
|
948
|
+
} catch (err) {
|
|
949
|
+
logger.debug(`[paths] Failed to stat entry ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch (err) {
|
|
953
|
+
logger.debug(`[paths] Failed to calculate directory size for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
954
|
+
return 0;
|
|
955
|
+
}
|
|
956
|
+
return total;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Formats byte values into human-readable strings (e.g. "86.4 MB", "1.42 MB", "42.5 MB").
|
|
960
|
+
*/
|
|
961
|
+
function formatBytes(bytes) {
|
|
962
|
+
if (bytes <= 0 || !Number.isFinite(bytes)) return "0 B";
|
|
963
|
+
if (bytes < 1) return `${Math.round(bytes)} B`;
|
|
964
|
+
const units = [
|
|
965
|
+
"B",
|
|
966
|
+
"KB",
|
|
967
|
+
"MB",
|
|
968
|
+
"GB",
|
|
969
|
+
"TB"
|
|
970
|
+
];
|
|
971
|
+
let i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
|
|
972
|
+
let val = bytes / Math.pow(1024, i);
|
|
973
|
+
if (Math.round(val) >= 1024 && i < units.length - 1) {
|
|
974
|
+
i++;
|
|
975
|
+
val /= 1024;
|
|
976
|
+
}
|
|
977
|
+
return `${i === 0 ? val.toFixed(0) : val < 10 ? val.toFixed(2) : val < 100 ? val.toFixed(1) : val.toFixed(0)} ${units[i]}`;
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/errors.ts
|
|
981
|
+
var NanosLintError = class extends Error {
|
|
982
|
+
code;
|
|
983
|
+
remedy;
|
|
984
|
+
name = "NanosLintError";
|
|
985
|
+
constructor(message, code, remedy, options) {
|
|
986
|
+
super(message, options);
|
|
987
|
+
this.code = code;
|
|
988
|
+
this.remedy = remedy;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
var ConfigError = class extends NanosLintError {
|
|
992
|
+
name = "ConfigError";
|
|
993
|
+
};
|
|
994
|
+
var LuaLSError = class extends NanosLintError {
|
|
995
|
+
name = "LuaLSError";
|
|
996
|
+
};
|
|
997
|
+
var AnnotationsError = class extends NanosLintError {
|
|
998
|
+
name = "AnnotationsError";
|
|
999
|
+
};
|
|
1000
|
+
var CacheError = class extends NanosLintError {
|
|
1001
|
+
name = "CacheError";
|
|
1002
|
+
};
|
|
868
1003
|
//#endregion
|
|
869
1004
|
//#region src/config.ts
|
|
870
1005
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -916,8 +1051,14 @@ function parseJsonc(text) {
|
|
|
916
1051
|
return result;
|
|
917
1052
|
}
|
|
918
1053
|
function loadConfigFile(filePath) {
|
|
919
|
-
if (!fs.existsSync(filePath)) throw new
|
|
920
|
-
|
|
1054
|
+
if (!fs.existsSync(filePath)) throw new ConfigError(`Configuration file not found: ${filePath}`, "ERR_CONFIG_NOT_FOUND", "Ensure the configuration file path is correct or run 'nanos-lint init' to generate a default config.");
|
|
1055
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1056
|
+
try {
|
|
1057
|
+
return parseJsonc(content);
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
if (err instanceof ConfigError) throw err;
|
|
1060
|
+
throw new ConfigError(`Failed to parse configuration file at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, "ERR_CONFIG_PARSE", "Check your .luarc.json syntax or run 'nanos-lint init --force' to scaffold a clean template.", { cause: err });
|
|
1061
|
+
}
|
|
921
1062
|
}
|
|
922
1063
|
/**
|
|
923
1064
|
* Removes trailing `/` characters from a path-like string.
|
|
@@ -933,6 +1074,76 @@ function stripTrailingSlashes(value) {
|
|
|
933
1074
|
return end === value.length ? value : value.slice(0, end);
|
|
934
1075
|
}
|
|
935
1076
|
/**
|
|
1077
|
+
* Set of recognized diagnostic codes supported by Lua Language Server (LuaLS).
|
|
1078
|
+
* LuaLS discards the entire `diagnostics.severity` object if it contains a single
|
|
1079
|
+
* unrecognized key (such as obsolete 'syntax-error'). nanos-lint filters severity
|
|
1080
|
+
* and neededFileStatus maps against this allow-list to ensure valid configuration.
|
|
1081
|
+
*/
|
|
1082
|
+
const VALID_LUALS_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
1083
|
+
"ambiguity-1",
|
|
1084
|
+
"assign-type-mismatch",
|
|
1085
|
+
"await-in-sync",
|
|
1086
|
+
"cast-local-type",
|
|
1087
|
+
"cast-type-mismatch",
|
|
1088
|
+
"circle-doc-class",
|
|
1089
|
+
"close-non-object",
|
|
1090
|
+
"code-after-break",
|
|
1091
|
+
"codestyle-check",
|
|
1092
|
+
"count-down-loop",
|
|
1093
|
+
"deprecated",
|
|
1094
|
+
"different-requires",
|
|
1095
|
+
"discard-returns",
|
|
1096
|
+
"doc-field-no-class",
|
|
1097
|
+
"duplicate-doc-alias",
|
|
1098
|
+
"duplicate-doc-field",
|
|
1099
|
+
"duplicate-doc-param",
|
|
1100
|
+
"duplicate-index",
|
|
1101
|
+
"duplicate-set-field",
|
|
1102
|
+
"empty-block",
|
|
1103
|
+
"global-element",
|
|
1104
|
+
"global-in-nil-env",
|
|
1105
|
+
"incomplete-signature-doc",
|
|
1106
|
+
"inject-field",
|
|
1107
|
+
"invisible",
|
|
1108
|
+
"lowercase-global",
|
|
1109
|
+
"missing-fields",
|
|
1110
|
+
"missing-global-doc",
|
|
1111
|
+
"missing-local-export-doc",
|
|
1112
|
+
"missing-parameter",
|
|
1113
|
+
"missing-return",
|
|
1114
|
+
"missing-return-value",
|
|
1115
|
+
"name-style-check",
|
|
1116
|
+
"need-check-nil",
|
|
1117
|
+
"newfield-call",
|
|
1118
|
+
"newline-call",
|
|
1119
|
+
"no-unknown",
|
|
1120
|
+
"not-yieldable",
|
|
1121
|
+
"param-type-mismatch",
|
|
1122
|
+
"redefined-local",
|
|
1123
|
+
"redundant-parameter",
|
|
1124
|
+
"redundant-return",
|
|
1125
|
+
"redundant-return-value",
|
|
1126
|
+
"redundant-value",
|
|
1127
|
+
"return-type-mismatch",
|
|
1128
|
+
"spell-check",
|
|
1129
|
+
"trailing-space",
|
|
1130
|
+
"unbalanced-assignments",
|
|
1131
|
+
"undefined-doc-class",
|
|
1132
|
+
"undefined-doc-name",
|
|
1133
|
+
"undefined-doc-param",
|
|
1134
|
+
"undefined-env-child",
|
|
1135
|
+
"undefined-field",
|
|
1136
|
+
"undefined-global",
|
|
1137
|
+
"unknown-cast-variable",
|
|
1138
|
+
"unknown-diag-code",
|
|
1139
|
+
"unknown-operator",
|
|
1140
|
+
"unreachable-code",
|
|
1141
|
+
"unused-function",
|
|
1142
|
+
"unused-label",
|
|
1143
|
+
"unused-local",
|
|
1144
|
+
"unused-vararg"
|
|
1145
|
+
]);
|
|
1146
|
+
/**
|
|
936
1147
|
* Merges a base nanos configuration with a workspace override configuration.
|
|
937
1148
|
* Guarantees that nanos API annotations are included in workspace.library,
|
|
938
1149
|
* and standardizes paths for LuaLS.
|
|
@@ -949,7 +1160,9 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
|
|
|
949
1160
|
const candidate = path.join(definitionsPath, "annotations.lua");
|
|
950
1161
|
if (fs.existsSync(candidate)) resolvedPath = candidate;
|
|
951
1162
|
}
|
|
952
|
-
} catch {
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
logger.debug(`Error checking annotations definitionsPath directory (${definitionsPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
+
}
|
|
953
1166
|
const normalizedDefPath = resolvedPath.split(path.sep).join("/");
|
|
954
1167
|
const baseLibraries = base.workspace?.library ?? [];
|
|
955
1168
|
const overrideLibraries = override.workspace?.library ?? [];
|
|
@@ -961,10 +1174,20 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
|
|
|
961
1174
|
const baseGlobals = base.diagnostics?.globals ?? [];
|
|
962
1175
|
const overrideGlobals = override.diagnostics?.globals ?? [];
|
|
963
1176
|
const globalsSet = /* @__PURE__ */ new Set([...baseGlobals, ...overrideGlobals]);
|
|
964
|
-
const
|
|
1177
|
+
const rawSeverity = {
|
|
965
1178
|
...base.diagnostics?.severity ?? {},
|
|
966
1179
|
...override.diagnostics?.severity ?? {}
|
|
967
1180
|
};
|
|
1181
|
+
const mergedSeverity = {};
|
|
1182
|
+
for (const [code, level] of Object.entries(rawSeverity)) if (VALID_LUALS_DIAGNOSTIC_CODES.has(code)) mergedSeverity[code] = level;
|
|
1183
|
+
else logger.warn(`[config] Unrecognized diagnostic code "${code}" in diagnostics.severity was dropped to prevent LuaLS from discarding the severity configuration.`);
|
|
1184
|
+
const rawNeededFileStatus = {
|
|
1185
|
+
...base.diagnostics?.neededFileStatus ?? {},
|
|
1186
|
+
...override.diagnostics?.neededFileStatus ?? {}
|
|
1187
|
+
};
|
|
1188
|
+
const mergedNeededFileStatus = {};
|
|
1189
|
+
for (const [code, status] of Object.entries(rawNeededFileStatus)) if (VALID_LUALS_DIAGNOSTIC_CODES.has(code)) mergedNeededFileStatus[code] = status;
|
|
1190
|
+
else logger.warn(`[config] Unrecognized diagnostic code "${code}" in diagnostics.neededFileStatus was dropped.`);
|
|
968
1191
|
const hasCliIgnore = Boolean(options?.cliIgnore && options.cliIgnore.length > 0);
|
|
969
1192
|
let mergedIgnoreDir;
|
|
970
1193
|
let mergedFilesExclude;
|
|
@@ -1037,7 +1260,8 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
|
|
|
1037
1260
|
...base.diagnostics ?? {},
|
|
1038
1261
|
...override.diagnostics ?? {},
|
|
1039
1262
|
globals: Array.from(globalsSet),
|
|
1040
|
-
severity: mergedSeverity
|
|
1263
|
+
severity: mergedSeverity,
|
|
1264
|
+
...Object.keys(mergedNeededFileStatus).length > 0 ? { neededFileStatus: mergedNeededFileStatus } : {}
|
|
1041
1265
|
}
|
|
1042
1266
|
};
|
|
1043
1267
|
}
|
|
@@ -1099,10 +1323,10 @@ function resolveWorkspaceConfig(workspacePath, customConfigPath, options) {
|
|
|
1099
1323
|
*/
|
|
1100
1324
|
function initWorkspace(workspacePath, options) {
|
|
1101
1325
|
const targetFile = path.join(workspacePath, ".luarc.json");
|
|
1102
|
-
if (fs.existsSync(targetFile) && !options?.force) throw new
|
|
1326
|
+
if (fs.existsSync(targetFile) && !options?.force) throw new ConfigError(`.luarc.json already exists at ${targetFile}. Use --force to overwrite.`, "ERR_CONFIG_EXISTS", "Pass --force to overwrite the existing .luarc.json file.");
|
|
1103
1327
|
const template = loadConfigFile(getDefaultTemplatePath());
|
|
1104
1328
|
const sourceAnnotations = options?.annotationsPath || getDefaultAnnotationsPath();
|
|
1105
|
-
if (!fs.existsSync(sourceAnnotations)) throw new
|
|
1329
|
+
if (!fs.existsSync(sourceAnnotations)) throw new ConfigError(`Definitions file not found at ${sourceAnnotations}. Make sure annotations have been downloaded or pass a valid file with --annotations.`, "ERR_ANNOTATIONS_NOT_FOUND", "Run 'nanos-lint warmup' to download annotations or provide --annotations <path>.");
|
|
1106
1330
|
const targetNanosDir = path.join(workspacePath, ".nanos-lint");
|
|
1107
1331
|
fs.mkdirSync(targetNanosDir, { recursive: true });
|
|
1108
1332
|
const targetAnnotations = path.join(targetNanosDir, "annotations.lua");
|
|
@@ -1118,8 +1342,7 @@ function initWorkspace(workspacePath, options) {
|
|
|
1118
1342
|
return targetFile;
|
|
1119
1343
|
}
|
|
1120
1344
|
//#endregion
|
|
1121
|
-
//#region src/luals.ts
|
|
1122
|
-
const execFileAsync = promisify(execFile);
|
|
1345
|
+
//#region src/luals/version.ts
|
|
1123
1346
|
const FALLBACK_LUALS_VERSION = "3.19.1";
|
|
1124
1347
|
const DEFAULT_LUALS_VERSION = "latest";
|
|
1125
1348
|
/**
|
|
@@ -1156,15 +1379,10 @@ function sanitizeLuaLSVersion(raw) {
|
|
|
1156
1379
|
return first >= 48 && first <= 57 || first >= 65 && first <= 90 || first >= 97 && first <= 122 ? version : null;
|
|
1157
1380
|
}
|
|
1158
1381
|
/**
|
|
1159
|
-
*
|
|
1382
|
+
* Fetches the latest available LuaLS release tag from the GitHub API.
|
|
1383
|
+
* Returns null if the request fails, times out, or receives an invalid tag.
|
|
1160
1384
|
*/
|
|
1161
|
-
function
|
|
1162
|
-
return str.replace(/'/g, "''");
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Resolves the latest available LuaLS release tag from the GitHub API.
|
|
1166
|
-
*/
|
|
1167
|
-
async function resolveLatestLuaLSVersion() {
|
|
1385
|
+
async function fetchLatestLuaLSVersionFromGitHub() {
|
|
1168
1386
|
try {
|
|
1169
1387
|
const headers = { "User-Agent": "nanos-lint" };
|
|
1170
1388
|
if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
|
@@ -1177,8 +1395,17 @@ async function resolveLatestLuaLSVersion() {
|
|
|
1177
1395
|
const version = typeof data.tag_name === "string" ? sanitizeLuaLSVersion(data.tag_name) : null;
|
|
1178
1396
|
if (version) return version;
|
|
1179
1397
|
}
|
|
1180
|
-
} catch {
|
|
1181
|
-
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
logger.debug(`[luals] Failed to resolve latest LuaLS version from GitHub API: ${err instanceof Error ? err.message : String(err)}`);
|
|
1400
|
+
}
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Resolves the latest available LuaLS release tag from the GitHub API,
|
|
1405
|
+
* falling back to FALLBACK_LUALS_VERSION if offline or unreachable.
|
|
1406
|
+
*/
|
|
1407
|
+
async function resolveLatestLuaLSVersion() {
|
|
1408
|
+
return await fetchLatestLuaLSVersionFromGitHub() || "3.19.1";
|
|
1182
1409
|
}
|
|
1183
1410
|
/**
|
|
1184
1411
|
* Resolves a version string ("latest" -> actual tag).
|
|
@@ -1188,9 +1415,11 @@ async function resolveLatestLuaLSVersion() {
|
|
|
1188
1415
|
async function resolveLuaLSVersion(version) {
|
|
1189
1416
|
if (!version || version === "latest") return await resolveLatestLuaLSVersion();
|
|
1190
1417
|
const sanitized = sanitizeLuaLSVersion(version);
|
|
1191
|
-
if (!sanitized) throw new
|
|
1418
|
+
if (!sanitized) throw new LuaLSError(`Invalid LuaLS version: "${version}". Expected a release tag such as "3.19.1", or "latest".`, "ERR_LUALS_INVALID_VERSION", "Provide a valid release tag like '3.19.1' or use 'latest'.");
|
|
1192
1419
|
return sanitized;
|
|
1193
1420
|
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/luals/platform.ts
|
|
1194
1423
|
function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
1195
1424
|
const platform = process.platform;
|
|
1196
1425
|
const arch = process.arch;
|
|
@@ -1201,7 +1430,7 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
|
1201
1430
|
assetName: `lua-language-server-${version}-win32-x64.zip`,
|
|
1202
1431
|
binaryRelativePath: path.join("bin", "lua-language-server.exe")
|
|
1203
1432
|
};
|
|
1204
|
-
throw new
|
|
1433
|
+
throw new LuaLSError(`Unsupported Windows architecture: ${arch}. Supported: x64`, "ERR_LUALS_UNSUPPORTED_ARCH", "LuaLS provides pre-built binaries for Windows x64. Use a compatible system or set LUALS_BIN to a custom binary.");
|
|
1205
1434
|
}
|
|
1206
1435
|
if (platform === "linux") {
|
|
1207
1436
|
if (arch === "x64") return {
|
|
@@ -1216,7 +1445,7 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
|
1216
1445
|
assetName: `lua-language-server-${version}-linux-arm64.tar.gz`,
|
|
1217
1446
|
binaryRelativePath: path.join("bin", "lua-language-server")
|
|
1218
1447
|
};
|
|
1219
|
-
throw new
|
|
1448
|
+
throw new LuaLSError(`Unsupported Linux architecture: ${arch}. Supported: x64, arm64`, "ERR_LUALS_UNSUPPORTED_ARCH", "LuaLS provides pre-built binaries for Linux x64 and arm64. Use LUALS_BIN to specify a custom build.");
|
|
1220
1449
|
}
|
|
1221
1450
|
if (platform === "darwin") return {
|
|
1222
1451
|
platform: "darwin",
|
|
@@ -1224,18 +1453,10 @@ function getPlatformInfo(version = FALLBACK_LUALS_VERSION) {
|
|
|
1224
1453
|
assetName: `lua-language-server-${version}-darwin-${arch === "arm64" ? "arm64" : "x64"}.tar.gz`,
|
|
1225
1454
|
binaryRelativePath: path.join("bin", "lua-language-server")
|
|
1226
1455
|
};
|
|
1227
|
-
throw new
|
|
1228
|
-
}
|
|
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) {
|
|
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");
|
|
1237
|
-
return path.join(base, "nanos-lint", "luals", version);
|
|
1456
|
+
throw new LuaLSError(`Unsupported platform: ${platform}`, "ERR_LUALS_UNSUPPORTED_PLATFORM", "LuaLS provides pre-built binaries for Windows, Linux, and macOS. Use LUALS_BIN to specify a custom build.");
|
|
1238
1457
|
}
|
|
1458
|
+
//#endregion
|
|
1459
|
+
//#region src/luals/validation.ts
|
|
1239
1460
|
/**
|
|
1240
1461
|
* Verifies that a LuaLS binary exists, has non-trivial size, and is executable.
|
|
1241
1462
|
*/
|
|
@@ -1250,10 +1471,198 @@ function isBinaryValid(binaryPath) {
|
|
|
1250
1471
|
encoding: "utf-8"
|
|
1251
1472
|
});
|
|
1252
1473
|
return /^\d+\.\d+\.\d+/.test(output.trim());
|
|
1253
|
-
} catch {
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1254
1476
|
return false;
|
|
1255
1477
|
}
|
|
1256
1478
|
}
|
|
1479
|
+
//#endregion
|
|
1480
|
+
//#region src/luals/cache.ts
|
|
1481
|
+
function getBaseLuaLSCacheDir() {
|
|
1482
|
+
return path.join(systemPaths.cache, "luals");
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Cache directory of a LuaLS version. `baseCacheDir` defaults to the system
|
|
1486
|
+
* cache and can be overridden to keep a caller isolated from it.
|
|
1487
|
+
*/
|
|
1488
|
+
function getCacheDir(version = FALLBACK_LUALS_VERSION, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1489
|
+
return path.join(baseCacheDir, version);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Returns the legacy cache directory used in nanos-lint <= 2.2.1.
|
|
1493
|
+
*/
|
|
1494
|
+
function getLegacyCacheDir(version = FALLBACK_LUALS_VERSION) {
|
|
1495
|
+
const base = process.platform === "win32" ? process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
1496
|
+
return path.join(base, "nanos-lint", "luals", version);
|
|
1497
|
+
}
|
|
1498
|
+
const LUALS_METADATA_FILENAME = "metadata.json";
|
|
1499
|
+
/**
|
|
1500
|
+
* Calculates the ISO 8601 week string for a given date in the format 'YYYY-Www' (e.g. '2026-W39').
|
|
1501
|
+
*/
|
|
1502
|
+
function getIsoWeek(d = /* @__PURE__ */ new Date()) {
|
|
1503
|
+
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
1504
|
+
const dayNum = date.getUTCDay() || 7;
|
|
1505
|
+
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
|
|
1506
|
+
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
1507
|
+
const weekNo = Math.ceil(((date.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
1508
|
+
return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
|
1509
|
+
}
|
|
1510
|
+
function getLuaLSMetadataPath(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1511
|
+
return path.join(baseCacheDir, LUALS_METADATA_FILENAME);
|
|
1512
|
+
}
|
|
1513
|
+
function readLuaLSMetadata(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1514
|
+
const metaPath = getLuaLSMetadataPath(baseCacheDir);
|
|
1515
|
+
if (!fs.existsSync(metaPath)) return null;
|
|
1516
|
+
try {
|
|
1517
|
+
const content = fs.readFileSync(metaPath, "utf-8");
|
|
1518
|
+
const parsed = JSON.parse(content);
|
|
1519
|
+
if (typeof parsed?.lastCheckedWeek === "string" && typeof parsed?.latestVersion === "string") return parsed;
|
|
1520
|
+
try {
|
|
1521
|
+
fs.unlinkSync(metaPath);
|
|
1522
|
+
logger.warn(`[luals] Stale or invalid LuaLS metadata at ${metaPath} purged.`);
|
|
1523
|
+
} catch (unlinkErr) {
|
|
1524
|
+
logger.debug(`[luals] Failed to unlink invalid metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
1525
|
+
}
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
logger.debug(`[luals] Failed to parse LuaLS metadata: ${err instanceof Error ? err.message : String(err)}`);
|
|
1528
|
+
try {
|
|
1529
|
+
fs.unlinkSync(metaPath);
|
|
1530
|
+
logger.warn(`[luals] Corrupted LuaLS metadata at ${metaPath} purged.`);
|
|
1531
|
+
} catch (unlinkErr) {
|
|
1532
|
+
logger.debug(`[luals] Failed to unlink corrupted metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
function writeLuaLSMetadata(metadata, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1538
|
+
try {
|
|
1539
|
+
fs.mkdirSync(baseCacheDir, { recursive: true });
|
|
1540
|
+
const metaPath = getLuaLSMetadataPath(baseCacheDir);
|
|
1541
|
+
fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
logger.warn(`[luals] Failed to write LuaLS metadata: ${err instanceof Error ? err.message : String(err)}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Lists all candidate LuaLS version directories under baseCacheDir without running
|
|
1548
|
+
* execution smoke tests on the binaries.
|
|
1549
|
+
*/
|
|
1550
|
+
function listCachedLuaLSVersionDirs(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1551
|
+
if (!fs.existsSync(baseCacheDir)) return [];
|
|
1552
|
+
try {
|
|
1553
|
+
const entries = fs.readdirSync(baseCacheDir, { withFileTypes: true });
|
|
1554
|
+
const versions = [];
|
|
1555
|
+
for (const entry of entries) {
|
|
1556
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1557
|
+
const version = sanitizeLuaLSVersion(entry.name);
|
|
1558
|
+
if (version) versions.push(version);
|
|
1559
|
+
}
|
|
1560
|
+
return versions;
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
logger.debug(`[luals] Failed to list cached LuaLS version directories: ${err instanceof Error ? err.message : String(err)}`);
|
|
1563
|
+
return [];
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Lists all cached LuaLS versions in baseCacheDir that have a valid .complete marker
|
|
1568
|
+
* and a functional executable binary.
|
|
1569
|
+
*/
|
|
1570
|
+
function listCachedLuaLSVersions(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1571
|
+
if (!fs.existsSync(baseCacheDir)) return [];
|
|
1572
|
+
try {
|
|
1573
|
+
const entries = fs.readdirSync(baseCacheDir, { withFileTypes: true });
|
|
1574
|
+
const versions = [];
|
|
1575
|
+
for (const entry of entries) {
|
|
1576
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1577
|
+
const version = sanitizeLuaLSVersion(entry.name);
|
|
1578
|
+
if (!version) continue;
|
|
1579
|
+
const dirPath = path.join(baseCacheDir, entry.name);
|
|
1580
|
+
const marker = path.join(dirPath, ".complete");
|
|
1581
|
+
const info = getPlatformInfo(version);
|
|
1582
|
+
const bin = path.join(dirPath, info.binaryRelativePath);
|
|
1583
|
+
if (fs.existsSync(marker) && fs.existsSync(bin)) try {
|
|
1584
|
+
if (fs.readFileSync(marker, "utf-8").trim() === version && isBinaryValid(bin)) versions.push(version);
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
logger.debug(`[luals] Error validating cached version ${version}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return versions;
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
logger.debug(`[luals] Failed to list cached LuaLS versions: ${err instanceof Error ? err.message : String(err)}`);
|
|
1592
|
+
return [];
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Removes older cached LuaLS version directories, preserving keepVersion,
|
|
1597
|
+
* hidden/temporary directories, and metadata files.
|
|
1598
|
+
*/
|
|
1599
|
+
function cleanupOldCachedLuaLSVersions(keepVersion, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1600
|
+
if (!fs.existsSync(baseCacheDir)) return [];
|
|
1601
|
+
const removed = [];
|
|
1602
|
+
try {
|
|
1603
|
+
const entries = fs.readdirSync(baseCacheDir, { withFileTypes: true });
|
|
1604
|
+
for (const entry of entries) {
|
|
1605
|
+
if (!entry.isDirectory()) continue;
|
|
1606
|
+
if (entry.name.startsWith(".")) continue;
|
|
1607
|
+
if (entry.name !== keepVersion) {
|
|
1608
|
+
const dirPath = path.join(baseCacheDir, entry.name);
|
|
1609
|
+
try {
|
|
1610
|
+
fs.rmSync(dirPath, {
|
|
1611
|
+
recursive: true,
|
|
1612
|
+
force: true,
|
|
1613
|
+
maxRetries: 5,
|
|
1614
|
+
retryDelay: 100
|
|
1615
|
+
});
|
|
1616
|
+
removed.push(entry.name);
|
|
1617
|
+
logger.info(`[luals] Cleaned up older cached LuaLS version: ${entry.name}`);
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
logger.warn(`[luals] Failed to remove older cached LuaLS version at ${dirPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
logger.debug(`[luals] Failed to clean up old LuaLS versions: ${err instanceof Error ? err.message : String(err)}`);
|
|
1625
|
+
}
|
|
1626
|
+
return removed;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Locates an existing, valid LuaLS directory for the specified version in the
|
|
1630
|
+
* primary cache (`baseCacheDir`), the legacy cache, or the package root.
|
|
1631
|
+
*/
|
|
1632
|
+
function findExistingLuaLSDir(version, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
1633
|
+
const info = getPlatformInfo(version);
|
|
1634
|
+
const primaryCache = getCacheDir(version, baseCacheDir);
|
|
1635
|
+
const primaryBin = path.join(primaryCache, info.binaryRelativePath);
|
|
1636
|
+
const primaryMarker = path.join(primaryCache, ".complete");
|
|
1637
|
+
if (fs.existsSync(primaryMarker)) try {
|
|
1638
|
+
if (fs.readFileSync(primaryMarker, "utf-8").trim() === version && isBinaryValid(primaryBin)) return primaryCache;
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
logger.debug(`[luals] Error checking primary LuaLS cache marker at ${primaryMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1641
|
+
}
|
|
1642
|
+
const legacyCache = getLegacyCacheDir(version);
|
|
1643
|
+
if (path.resolve(legacyCache) !== path.resolve(primaryCache)) {
|
|
1644
|
+
const legacyBin = path.join(legacyCache, info.binaryRelativePath);
|
|
1645
|
+
const legacyMarker = path.join(legacyCache, ".complete");
|
|
1646
|
+
if (fs.existsSync(legacyMarker)) try {
|
|
1647
|
+
if (fs.readFileSync(legacyMarker, "utf-8").trim() === version && isBinaryValid(legacyBin)) return legacyCache;
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
logger.debug(`[luals] Error checking legacy LuaLS cache marker at ${legacyMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const pkgRoot = getPackageRoot();
|
|
1653
|
+
const pkgBin = path.join(pkgRoot, info.binaryRelativePath);
|
|
1654
|
+
if (fs.existsSync(pkgBin) && isBinaryValid(pkgBin)) return pkgRoot;
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/luals/download.ts
|
|
1659
|
+
const execFileAsync$1 = promisify(execFile);
|
|
1660
|
+
/**
|
|
1661
|
+
* Escapes single quotes for safe PowerShell single-quoted string interpolation.
|
|
1662
|
+
*/
|
|
1663
|
+
function escapePowerShellSingleQuote(str) {
|
|
1664
|
+
return str.replace(/'/g, "''");
|
|
1665
|
+
}
|
|
1257
1666
|
async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDir, options) {
|
|
1258
1667
|
const resolvedVersion = await resolveLuaLSVersion(version);
|
|
1259
1668
|
const info = getPlatformInfo(resolvedVersion);
|
|
@@ -1261,76 +1670,86 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1261
1670
|
const binaryPath = path.join(destDir, info.binaryRelativePath);
|
|
1262
1671
|
const completeMarker = path.join(destDir, ".complete");
|
|
1263
1672
|
if (fs.existsSync(destDir)) {
|
|
1264
|
-
if (fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
|
|
1673
|
+
if (options?.reuseExisting !== false && fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
|
|
1265
1674
|
if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(binaryPath)) return binaryPath;
|
|
1266
|
-
} catch {
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1677
|
+
}
|
|
1267
1678
|
try {
|
|
1268
1679
|
fs.rmSync(destDir, {
|
|
1269
1680
|
recursive: true,
|
|
1270
1681
|
force: true
|
|
1271
1682
|
});
|
|
1272
|
-
} catch {
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
logger.warn(`[luals] Failed to remove stale or invalid cache dir ${destDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1685
|
+
}
|
|
1273
1686
|
}
|
|
1274
1687
|
const parentDir = path.dirname(destDir);
|
|
1275
1688
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
1276
1689
|
const tempDir = path.join(parentDir, `.${path.basename(destDir)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
1277
1690
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
1691
|
+
const existingSourceDir = options?.reuseExisting !== false ? findExistingLuaLSDir(resolvedVersion, options?.cacheDir ?? getBaseLuaLSCacheDir()) : null;
|
|
1692
|
+
const shouldCopyFromExisting = existingSourceDir !== null && path.resolve(existingSourceDir) !== path.resolve(destDir);
|
|
1278
1693
|
const url = `https://github.com/LuaLS/lua-language-server/releases/download/${resolvedVersion}/${info.assetName}`;
|
|
1279
1694
|
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
1695
|
try {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1696
|
+
if (shouldCopyFromExisting) {
|
|
1697
|
+
if (!options?.quiet) logger.info(`[luals] Reusing existing LuaLS ${resolvedVersion} installation from ${existingSourceDir}...`);
|
|
1698
|
+
fs.cpSync(existingSourceDir, tempDir, { recursive: true });
|
|
1699
|
+
} else {
|
|
1700
|
+
if (!options?.quiet) logger.info(`[luals] Downloading LuaLS ${resolvedVersion} from ${url}...`);
|
|
1701
|
+
let response = null;
|
|
1702
|
+
let lastErr = null;
|
|
1703
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1704
|
+
try {
|
|
1705
|
+
const res = await fetch(url);
|
|
1706
|
+
if (res.ok && res.body) {
|
|
1707
|
+
response = res;
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
await res.body?.cancel();
|
|
1711
|
+
lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
lastErr = err;
|
|
1714
|
+
}
|
|
1715
|
+
if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
|
|
1716
|
+
}
|
|
1717
|
+
if (!response || !response.body) {
|
|
1718
|
+
if (lastErr instanceof LuaLSError) throw lastErr;
|
|
1719
|
+
throw new LuaLSError(`Failed to download LuaLS from ${url}${lastErr ? `: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}` : ""}`, "ERR_LUALS_DOWNLOAD", "Check your network connection or specify a custom binary with LUALS_BIN.", { cause: lastErr });
|
|
1720
|
+
}
|
|
1721
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1722
|
+
fs.writeFileSync(archivePath, Buffer.from(arrayBuffer));
|
|
1723
|
+
if (!options?.quiet) logger.info(`[luals] Extracting to ${destDir}...`);
|
|
1724
|
+
try {
|
|
1725
|
+
await execFileAsync$1("tar", [
|
|
1726
|
+
"-xf",
|
|
1727
|
+
archivePath,
|
|
1728
|
+
"-C",
|
|
1729
|
+
tempDir
|
|
1730
|
+
]);
|
|
1731
|
+
} catch (tarErr) {
|
|
1732
|
+
if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync$1("powershell.exe", [
|
|
1733
|
+
"-NoProfile",
|
|
1734
|
+
"-Command",
|
|
1735
|
+
`Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
|
|
1736
|
+
]);
|
|
1737
|
+
else throw tarErr;
|
|
1738
|
+
}
|
|
1739
|
+
try {
|
|
1740
|
+
fs.unlinkSync(archivePath);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
logger.debug(`[luals] Failed to delete temporary archive ${archivePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1743
|
+
}
|
|
1324
1744
|
}
|
|
1325
|
-
try {
|
|
1326
|
-
fs.unlinkSync(archivePath);
|
|
1327
|
-
} catch {}
|
|
1328
1745
|
const tempBinaryPath = path.join(tempDir, info.binaryRelativePath);
|
|
1329
1746
|
if (process.platform !== "win32") try {
|
|
1330
1747
|
fs.chmodSync(tempBinaryPath, 493);
|
|
1331
|
-
} catch {
|
|
1332
|
-
|
|
1333
|
-
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
logger.warn(`[luals] Failed to chmod binary at ${tempBinaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1750
|
+
}
|
|
1751
|
+
if (!fs.existsSync(tempBinaryPath) || fs.statSync(tempBinaryPath).size < 1e5) throw new LuaLSError(`Failed to extract valid LuaLS binary to expected path: ${tempBinaryPath}`, "ERR_LUALS_EXTRACT", "Run 'nanos-lint clean-cache' and ensure there is sufficient disk space.");
|
|
1752
|
+
if (!isBinaryValid(tempBinaryPath)) throw new LuaLSError(`Extracted LuaLS binary at ${tempBinaryPath} is invalid or non-functional.`, "ERR_LUALS_INVALID_BINARY", "Run 'nanos-lint clean-cache' to remove corrupted downloads or set LUALS_BIN to a custom binary.");
|
|
1334
1753
|
fs.writeFileSync(path.join(tempDir, ".complete"), resolvedVersion, "utf-8");
|
|
1335
1754
|
for (let attempt = 0; attempt < 5; attempt++) try {
|
|
1336
1755
|
fs.renameSync(tempDir, destDir);
|
|
@@ -1342,8 +1761,10 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1342
1761
|
recursive: true,
|
|
1343
1762
|
force: true
|
|
1344
1763
|
});
|
|
1345
|
-
} catch {
|
|
1346
|
-
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
logger.debug(`[luals] Failed to remove temp directory after concurrent promotion: ${err instanceof Error ? err.message : String(err)}`);
|
|
1766
|
+
}
|
|
1767
|
+
if (!options?.quiet) logger.info(`[luals] Ready: ${binaryPath}`);
|
|
1347
1768
|
return binaryPath;
|
|
1348
1769
|
}
|
|
1349
1770
|
if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
@@ -1353,11 +1774,13 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1353
1774
|
recursive: true,
|
|
1354
1775
|
force: true
|
|
1355
1776
|
});
|
|
1356
|
-
} catch {
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
logger.warn(`[luals] Failed to clean up broken destination directory ${destDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1779
|
+
}
|
|
1357
1780
|
throw renameErr;
|
|
1358
1781
|
}
|
|
1359
1782
|
}
|
|
1360
|
-
if (!options?.quiet)
|
|
1783
|
+
if (!options?.quiet) logger.info(`[luals] Ready: ${binaryPath}`);
|
|
1361
1784
|
return binaryPath;
|
|
1362
1785
|
} finally {
|
|
1363
1786
|
if (fs.existsSync(tempDir)) try {
|
|
@@ -1365,137 +1788,13 @@ async function downloadAndExtractLuaLS(version = DEFAULT_LUALS_VERSION, targetDi
|
|
|
1365
1788
|
recursive: true,
|
|
1366
1789
|
force: true
|
|
1367
1790
|
});
|
|
1368
|
-
} catch {
|
|
1369
|
-
|
|
1370
|
-
}
|
|
1371
|
-
async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
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
|
-
}
|
|
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...`);
|
|
1390
|
-
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;
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
try {
|
|
1416
|
-
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
1417
|
-
const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
|
|
1418
|
-
const found = stdout.trim().split(/\r?\n/)[0];
|
|
1419
|
-
if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
|
|
1420
|
-
} catch {}
|
|
1421
|
-
return await downloadAndExtractLuaLS(resolvedVersion, void 0, options);
|
|
1422
|
-
}
|
|
1423
|
-
async function runLuaLSCheck(targetPath, configPath, options) {
|
|
1424
|
-
const absoluteTarget = path.resolve(targetPath);
|
|
1425
|
-
if (!fs.existsSync(absoluteTarget)) throw new Error(`Target path does not exist: ${targetPath}`);
|
|
1426
|
-
const binary = options.lualsBin || await resolveLuaLSBinary(options.lualsVersion, { quiet: options.quiet });
|
|
1427
|
-
let checkDir = absoluteTarget;
|
|
1428
|
-
let targetFileOnly = null;
|
|
1429
|
-
if (fs.statSync(absoluteTarget).isFile()) {
|
|
1430
|
-
checkDir = path.dirname(absoluteTarget);
|
|
1431
|
-
targetFileOnly = absoluteTarget;
|
|
1432
|
-
}
|
|
1433
|
-
const tempOutputDir = systemPaths.temp;
|
|
1434
|
-
fs.mkdirSync(tempOutputDir, { recursive: true });
|
|
1435
|
-
const checkOutPath = path.join(tempOutputDir, `check-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
1436
|
-
const args = [
|
|
1437
|
-
`--check=${checkDir}`,
|
|
1438
|
-
`--configpath=${path.resolve(configPath)}`,
|
|
1439
|
-
`--check_out_path=${checkOutPath}`,
|
|
1440
|
-
"--check_format=json"
|
|
1441
|
-
];
|
|
1442
|
-
if (options.checklevel) args.push(`--checklevel=${options.checklevel}`);
|
|
1443
|
-
let execError = null;
|
|
1444
|
-
try {
|
|
1445
|
-
await execFileAsync(binary, args, {
|
|
1446
|
-
maxBuffer: 104857600,
|
|
1447
|
-
timeout: 12e4
|
|
1448
|
-
});
|
|
1449
|
-
} catch (err) {
|
|
1450
|
-
execError = err;
|
|
1451
|
-
}
|
|
1452
|
-
let diagnostics = {};
|
|
1453
|
-
let parseSucceeded = false;
|
|
1454
|
-
if (fs.existsSync(checkOutPath)) try {
|
|
1455
|
-
const content = fs.readFileSync(checkOutPath, "utf-8");
|
|
1456
|
-
diagnostics = JSON.parse(content);
|
|
1457
|
-
parseSucceeded = true;
|
|
1458
|
-
} catch {} finally {
|
|
1459
|
-
try {
|
|
1460
|
-
fs.unlinkSync(checkOutPath);
|
|
1461
|
-
} catch {}
|
|
1462
|
-
}
|
|
1463
|
-
if (!parseSucceeded) {
|
|
1464
|
-
const cacheHint = `(Cache location: ${getCacheDir()})`;
|
|
1465
|
-
if (execError) throw new Error(`LuaLS check failed to execute or produce diagnostic output: ${execError instanceof Error ? execError.message : String(execError)}. ${cacheHint}`);
|
|
1466
|
-
throw new Error(`LuaLS check failed to produce diagnostic output at: ${checkOutPath}. ${cacheHint}`);
|
|
1467
|
-
}
|
|
1468
|
-
if (targetFileOnly) {
|
|
1469
|
-
const filtered = {};
|
|
1470
|
-
const normTarget = path.resolve(targetFileOnly).toLowerCase();
|
|
1471
|
-
for (const [rawUri, diags] of Object.entries(diagnostics)) {
|
|
1472
|
-
const filePath = fileUriToPath(rawUri);
|
|
1473
|
-
if (path.resolve(filePath).toLowerCase() === normTarget) filtered[rawUri] = diags;
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
logger.debug(`[luals] Failed to clean up temporary directory ${tempDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1474
1793
|
}
|
|
1475
|
-
diagnostics = filtered;
|
|
1476
1794
|
}
|
|
1477
|
-
let totalProblems = 0;
|
|
1478
|
-
let totalErrors = 0;
|
|
1479
|
-
let totalWarnings = 0;
|
|
1480
|
-
let problemFiles = 0;
|
|
1481
|
-
for (const [_, diags] of Object.entries(diagnostics)) if (diags.length > 0) {
|
|
1482
|
-
problemFiles += 1;
|
|
1483
|
-
totalProblems += diags.length;
|
|
1484
|
-
for (const d of diags) if (d.severity === 1) totalErrors += 1;
|
|
1485
|
-
else if (d.severity === 2) totalWarnings += 1;
|
|
1486
|
-
}
|
|
1487
|
-
const passed = totalProblems === 0;
|
|
1488
|
-
const filesChecked = countCheckedFiles(targetPath, configPath);
|
|
1489
|
-
return {
|
|
1490
|
-
passed,
|
|
1491
|
-
totalProblems,
|
|
1492
|
-
totalErrors,
|
|
1493
|
-
totalWarnings,
|
|
1494
|
-
totalFiles: passed ? filesChecked : problemFiles,
|
|
1495
|
-
totalFilesChecked: filesChecked,
|
|
1496
|
-
diagnostics
|
|
1497
|
-
};
|
|
1498
1795
|
}
|
|
1796
|
+
//#endregion
|
|
1797
|
+
//#region src/luals/files.ts
|
|
1499
1798
|
/**
|
|
1500
1799
|
* Counts candidate Lua files within targetPath, taking ignoreDir and files.exclude into account.
|
|
1501
1800
|
*/
|
|
@@ -1514,7 +1813,9 @@ function countCheckedFiles(targetPath, configPath) {
|
|
|
1514
1813
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1515
1814
|
if (cfg.workspace?.ignoreDir) ignoreDirs = cfg.workspace.ignoreDir;
|
|
1516
1815
|
if (cfg.files?.exclude) excludePatterns = cfg.files.exclude;
|
|
1517
|
-
} catch {
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
logger.warn(`[luals] Failed to parse config file for file counting at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1818
|
+
}
|
|
1518
1819
|
const normIgnoreDirs = new Set(ignoreDirs.map((d) => d.replace(/\\/g, "/").toLowerCase()));
|
|
1519
1820
|
function isExcluded(relPath) {
|
|
1520
1821
|
const norm = relPath.replace(/\\/g, "/");
|
|
@@ -1532,7 +1833,9 @@ function countCheckedFiles(targetPath, configPath) {
|
|
|
1532
1833
|
const baseRegexStr = "^" + normPat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
1533
1834
|
try {
|
|
1534
1835
|
if (new RegExp(baseRegexStr, "i").test(baseName)) return true;
|
|
1535
|
-
} catch {
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
logger.debug(`[luals] Invalid regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1838
|
+
}
|
|
1536
1839
|
}
|
|
1537
1840
|
let regexStr = normPat;
|
|
1538
1841
|
const hasLeadingDoubleStar = regexStr.startsWith("**/");
|
|
@@ -1544,7 +1847,9 @@ function countCheckedFiles(targetPath, configPath) {
|
|
|
1544
1847
|
if (hasTrailingDoubleStar) escaped = `${escaped}(?:/.*)?`;
|
|
1545
1848
|
try {
|
|
1546
1849
|
if (new RegExp(`^${escaped}$`, "i").test(norm)) return true;
|
|
1547
|
-
} catch {
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
logger.debug(`[luals] Invalid glob regex for pattern "${normPat}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1852
|
+
}
|
|
1548
1853
|
}
|
|
1549
1854
|
}
|
|
1550
1855
|
return false;
|
|
@@ -1554,7 +1859,8 @@ function countCheckedFiles(targetPath, configPath) {
|
|
|
1554
1859
|
let entries;
|
|
1555
1860
|
try {
|
|
1556
1861
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1557
|
-
} catch {
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
logger.debug(`[luals] Failed to read directory ${currentDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1558
1864
|
return;
|
|
1559
1865
|
}
|
|
1560
1866
|
for (const entry of entries) {
|
|
@@ -1564,7 +1870,8 @@ function countCheckedFiles(targetPath, configPath) {
|
|
|
1564
1870
|
if (entry.isDirectory() || entry.isSymbolicLink() && (() => {
|
|
1565
1871
|
try {
|
|
1566
1872
|
return fs.statSync(fullPath).isDirectory();
|
|
1567
|
-
} catch {
|
|
1873
|
+
} catch (err) {
|
|
1874
|
+
logger.debug(`[luals] Failed to stat symlink target ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1568
1875
|
return false;
|
|
1569
1876
|
}
|
|
1570
1877
|
})()) {
|
|
@@ -1581,6 +1888,257 @@ function countCheckedFiles(targetPath, configPath) {
|
|
|
1581
1888
|
return count;
|
|
1582
1889
|
}
|
|
1583
1890
|
//#endregion
|
|
1891
|
+
//#region src/luals/runner.ts
|
|
1892
|
+
const execFileAsync = promisify(execFile);
|
|
1893
|
+
function isOfflineError(err) {
|
|
1894
|
+
if (!err) return false;
|
|
1895
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1896
|
+
return /offline|enotfound|eai_again|econnrefused|etimedout|fetch failed/i.test(msg);
|
|
1897
|
+
}
|
|
1898
|
+
async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
1899
|
+
if (process.env.LUALS_BIN && fs.existsSync(process.env.LUALS_BIN)) return process.env.LUALS_BIN;
|
|
1900
|
+
if (options?.reuseExisting !== false && (!version || version === "latest")) {
|
|
1901
|
+
const defaultInfo = getPlatformInfo(FALLBACK_LUALS_VERSION);
|
|
1902
|
+
const defaultBundledPath = path.join(getPackageRoot(), defaultInfo.binaryRelativePath);
|
|
1903
|
+
if (fs.existsSync(defaultBundledPath) && isBinaryValid(defaultBundledPath)) return defaultBundledPath;
|
|
1904
|
+
}
|
|
1905
|
+
const isDefaultOrLatest = !version || version === "latest";
|
|
1906
|
+
const baseCacheDir = options?.cacheDir ?? getBaseLuaLSCacheDir();
|
|
1907
|
+
if (!isDefaultOrLatest) {
|
|
1908
|
+
const resolvedVersion = await resolveLuaLSVersion(version);
|
|
1909
|
+
const info = getPlatformInfo(resolvedVersion);
|
|
1910
|
+
if (options?.reuseExisting !== false) {
|
|
1911
|
+
const bundledPath = path.join(getPackageRoot(), info.binaryRelativePath);
|
|
1912
|
+
if (fs.existsSync(bundledPath) && isBinaryValid(bundledPath)) return bundledPath;
|
|
1913
|
+
}
|
|
1914
|
+
const cachedDir = getCacheDir(resolvedVersion, baseCacheDir);
|
|
1915
|
+
const cachedPath = path.join(cachedDir, info.binaryRelativePath);
|
|
1916
|
+
const completeMarker = path.join(cachedDir, ".complete");
|
|
1917
|
+
let wasCorrupted = false;
|
|
1918
|
+
if (fs.existsSync(cachedPath)) {
|
|
1919
|
+
let isValid = false;
|
|
1920
|
+
if (fs.existsSync(completeMarker)) try {
|
|
1921
|
+
if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(cachedPath)) {
|
|
1922
|
+
isValid = true;
|
|
1923
|
+
if (options?.reuseExisting !== false) return cachedPath;
|
|
1924
|
+
}
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1927
|
+
}
|
|
1928
|
+
if (!isValid) {
|
|
1929
|
+
wasCorrupted = true;
|
|
1930
|
+
if (!options?.quiet) logger.warn(`[luals] Cached LuaLS binary at ${cachedPath} is corrupted or incomplete. Repairing...`);
|
|
1931
|
+
}
|
|
1932
|
+
try {
|
|
1933
|
+
fs.rmSync(cachedDir, {
|
|
1934
|
+
recursive: true,
|
|
1935
|
+
force: true
|
|
1936
|
+
});
|
|
1937
|
+
} catch (err) {
|
|
1938
|
+
logger.warn(`[luals] Failed to remove corrupted cache directory ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if (options?.reuseExisting !== false) {
|
|
1942
|
+
const legacyDir = getLegacyCacheDir(resolvedVersion);
|
|
1943
|
+
if (path.resolve(legacyDir) !== path.resolve(cachedDir)) {
|
|
1944
|
+
const legacyPath = path.join(legacyDir, info.binaryRelativePath);
|
|
1945
|
+
const legacyMarker = path.join(legacyDir, ".complete");
|
|
1946
|
+
if (fs.existsSync(legacyPath)) {
|
|
1947
|
+
let validLegacy = false;
|
|
1948
|
+
if (fs.existsSync(legacyMarker)) try {
|
|
1949
|
+
if (fs.readFileSync(legacyMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(legacyPath)) validLegacy = true;
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
logger.debug(`[luals] Failed to read legacy complete marker at ${legacyMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1952
|
+
}
|
|
1953
|
+
else if (isBinaryValid(legacyPath)) validLegacy = true;
|
|
1954
|
+
if (validLegacy) {
|
|
1955
|
+
try {
|
|
1956
|
+
fs.mkdirSync(path.dirname(cachedDir), { recursive: true });
|
|
1957
|
+
fs.cpSync(legacyDir, cachedDir, { recursive: true });
|
|
1958
|
+
if (fs.existsSync(cachedPath) && isBinaryValid(cachedPath)) return cachedPath;
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
logger.warn(`[luals] Failed to migrate legacy cache from ${legacyDir} to ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1961
|
+
}
|
|
1962
|
+
return legacyPath;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (options?.reuseExisting !== false) try {
|
|
1968
|
+
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
1969
|
+
const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
|
|
1970
|
+
const found = stdout.trim().split(/\r?\n/)[0];
|
|
1971
|
+
if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
|
|
1974
|
+
}
|
|
1975
|
+
try {
|
|
1976
|
+
return await downloadAndExtractLuaLS(resolvedVersion, getCacheDir(resolvedVersion, baseCacheDir), options);
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
if (wasCorrupted) {
|
|
1979
|
+
const isOffline = isOfflineError(err);
|
|
1980
|
+
const reason = isOffline ? "while offline" : err instanceof Error ? err.message : String(err);
|
|
1981
|
+
throw new LuaLSError(`Cached LuaLS binary at '${cachedPath}' is corrupted (failed execution/size check) and cannot be re-downloaded ${isOffline ? reason : `: ${reason}`}. Please ${isOffline ? "connect to the internet" : "verify your network connection"} to repair or run 'nanos-lint clean-cache'.`, "ERR_LUALS_CORRUPTED_CACHE", "Connect to the internet to repair the corrupted binary or run 'nanos-lint clean-cache'.", { cause: err });
|
|
1982
|
+
}
|
|
1983
|
+
throw err;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const currentWeek = getIsoWeek();
|
|
1987
|
+
const metadata = readLuaLSMetadata(baseCacheDir);
|
|
1988
|
+
if (options?.reuseExisting !== false && metadata && metadata.lastCheckedWeek === currentWeek && metadata.latestVersion) {
|
|
1989
|
+
const info = getPlatformInfo(metadata.latestVersion);
|
|
1990
|
+
const cachedPath = path.join(getCacheDir(metadata.latestVersion, baseCacheDir), info.binaryRelativePath);
|
|
1991
|
+
if (isBinaryValid(cachedPath)) return cachedPath;
|
|
1992
|
+
}
|
|
1993
|
+
const cachedVersions = options?.reuseExisting !== false ? listCachedLuaLSVersions(baseCacheDir) : [];
|
|
1994
|
+
const firstCachedVersion = cachedVersions[0];
|
|
1995
|
+
if (options?.reuseExisting !== false && metadata && metadata.lastCheckedWeek === currentWeek && firstCachedVersion) {
|
|
1996
|
+
const info = getPlatformInfo(firstCachedVersion);
|
|
1997
|
+
return path.join(getCacheDir(firstCachedVersion, baseCacheDir), info.binaryRelativePath);
|
|
1998
|
+
}
|
|
1999
|
+
const onlineTag = await fetchLatestLuaLSVersionFromGitHub();
|
|
2000
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2001
|
+
let targetVersion;
|
|
2002
|
+
if (onlineTag) targetVersion = onlineTag;
|
|
2003
|
+
else if (metadata?.latestVersion && cachedVersions.includes(metadata.latestVersion)) {
|
|
2004
|
+
logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${metadata.latestVersion}.`);
|
|
2005
|
+
targetVersion = metadata.latestVersion;
|
|
2006
|
+
} else if (firstCachedVersion) {
|
|
2007
|
+
logger.info(`[luals] Network unreachable or rate limited; using cached LuaLS ${firstCachedVersion}.`);
|
|
2008
|
+
targetVersion = firstCachedVersion;
|
|
2009
|
+
} else targetVersion = FALLBACK_LUALS_VERSION;
|
|
2010
|
+
writeLuaLSMetadata({
|
|
2011
|
+
lastCheckedWeek: currentWeek,
|
|
2012
|
+
latestVersion: targetVersion,
|
|
2013
|
+
lastCheckedDate: today
|
|
2014
|
+
}, baseCacheDir);
|
|
2015
|
+
const info = getPlatformInfo(targetVersion);
|
|
2016
|
+
const targetCacheDir = getCacheDir(targetVersion, baseCacheDir);
|
|
2017
|
+
const targetBinaryPath = path.join(targetCacheDir, info.binaryRelativePath);
|
|
2018
|
+
const completeMarker = path.join(targetCacheDir, ".complete");
|
|
2019
|
+
let wasCorrupted = false;
|
|
2020
|
+
if (fs.existsSync(targetBinaryPath)) {
|
|
2021
|
+
if (fs.existsSync(completeMarker) && isBinaryValid(targetBinaryPath)) {
|
|
2022
|
+
if (options?.reuseExisting !== false) {
|
|
2023
|
+
cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
|
|
2024
|
+
return targetBinaryPath;
|
|
2025
|
+
}
|
|
2026
|
+
} else wasCorrupted = true;
|
|
2027
|
+
}
|
|
2028
|
+
if (options?.reuseExisting !== false && !onlineTag) try {
|
|
2029
|
+
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
2030
|
+
const { stdout } = await execFileAsync(cmd, ["lua-language-server"]);
|
|
2031
|
+
const found = stdout.trim().split(/\r?\n/)[0];
|
|
2032
|
+
if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
|
|
2035
|
+
}
|
|
2036
|
+
let downloadedBinary;
|
|
2037
|
+
try {
|
|
2038
|
+
downloadedBinary = await downloadAndExtractLuaLS(targetVersion, targetCacheDir, options);
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
if (wasCorrupted) {
|
|
2041
|
+
const isOffline = isOfflineError(err);
|
|
2042
|
+
const reason = isOffline ? "while offline" : err instanceof Error ? err.message : String(err);
|
|
2043
|
+
throw new LuaLSError(`Cached LuaLS binary at '${targetBinaryPath}' is corrupted (failed execution/size check) and cannot be re-downloaded ${isOffline ? reason : `: ${reason}`}. Please ${isOffline ? "connect to the internet" : "verify your network connection"} to repair or run 'nanos-lint clean-cache'.`, "ERR_LUALS_CORRUPTED_CACHE", "Connect to the internet to repair the corrupted binary or run 'nanos-lint clean-cache'.", { cause: err });
|
|
2044
|
+
}
|
|
2045
|
+
throw err;
|
|
2046
|
+
}
|
|
2047
|
+
cleanupOldCachedLuaLSVersions(targetVersion, baseCacheDir);
|
|
2048
|
+
return downloadedBinary;
|
|
2049
|
+
}
|
|
2050
|
+
async function runLuaLSCheck(targetPath, configPath, options) {
|
|
2051
|
+
let absoluteTarget = path.resolve(targetPath);
|
|
2052
|
+
try {
|
|
2053
|
+
if (fs.existsSync(absoluteTarget)) absoluteTarget = fs.realpathSync.native(absoluteTarget);
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
logger.debug(`[luals] Failed to resolve native realpath for target: ${err instanceof Error ? err.message : String(err)}`);
|
|
2056
|
+
}
|
|
2057
|
+
if (!fs.existsSync(absoluteTarget)) throw new LuaLSError(`Target path does not exist: ${targetPath}`, "ERR_TARGET_NOT_FOUND", "Verify that the target path exists and is accessible.");
|
|
2058
|
+
const binary = options.lualsBin || await resolveLuaLSBinary(options.lualsVersion, { quiet: options.quiet });
|
|
2059
|
+
let checkDir = absoluteTarget;
|
|
2060
|
+
let targetFileOnly = null;
|
|
2061
|
+
if (fs.statSync(absoluteTarget).isFile()) {
|
|
2062
|
+
checkDir = path.dirname(absoluteTarget);
|
|
2063
|
+
targetFileOnly = absoluteTarget;
|
|
2064
|
+
}
|
|
2065
|
+
const tempOutputDir = systemPaths.temp;
|
|
2066
|
+
fs.mkdirSync(tempOutputDir, { recursive: true });
|
|
2067
|
+
const checkOutPath = path.join(tempOutputDir, `check-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
2068
|
+
const args = [
|
|
2069
|
+
`--check=${checkDir}`,
|
|
2070
|
+
`--configpath=${path.resolve(configPath)}`,
|
|
2071
|
+
`--check_out_path=${checkOutPath}`,
|
|
2072
|
+
"--check_format=json"
|
|
2073
|
+
];
|
|
2074
|
+
if (options.checklevel) args.push(`--checklevel=${options.checklevel}`);
|
|
2075
|
+
let execError = null;
|
|
2076
|
+
try {
|
|
2077
|
+
await execFileAsync(binary, args, {
|
|
2078
|
+
maxBuffer: 104857600,
|
|
2079
|
+
timeout: 12e4
|
|
2080
|
+
});
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
execError = err;
|
|
2083
|
+
logger.debug(`[luals] LuaLS process exited with error or non-zero status: ${err instanceof Error ? err.message : String(err)}`);
|
|
2084
|
+
}
|
|
2085
|
+
let diagnostics = {};
|
|
2086
|
+
let parseSucceeded = false;
|
|
2087
|
+
if (fs.existsSync(checkOutPath)) try {
|
|
2088
|
+
const content = fs.readFileSync(checkOutPath, "utf-8");
|
|
2089
|
+
diagnostics = JSON.parse(content);
|
|
2090
|
+
parseSucceeded = true;
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
logger.error(`[luals] Failed to read or parse diagnostic output from ${checkOutPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2093
|
+
} finally {
|
|
2094
|
+
try {
|
|
2095
|
+
fs.unlinkSync(checkOutPath);
|
|
2096
|
+
} catch (err) {
|
|
2097
|
+
logger.debug(`[luals] Failed to delete check output file ${checkOutPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (!parseSucceeded) {
|
|
2101
|
+
const cacheHint = `(Cache location: ${getCacheDir()})`;
|
|
2102
|
+
if (execError) throw new LuaLSError(`LuaLS check failed to execute or produce diagnostic output: ${execError instanceof Error ? execError.message : String(execError)}. ${cacheHint}`, "ERR_LUALS_EXECUTION", "Inspect the debug log with --log-level=debug or run 'nanos-lint clean-cache' to re-fetch LuaLS.", { cause: execError });
|
|
2103
|
+
throw new LuaLSError(`LuaLS check failed to produce diagnostic output at: ${checkOutPath}. ${cacheHint}`, "ERR_LUALS_NO_OUTPUT", "Ensure the temporary directory is writable and sufficient disk space is available.");
|
|
2104
|
+
}
|
|
2105
|
+
if (targetFileOnly) {
|
|
2106
|
+
const filtered = {};
|
|
2107
|
+
const normalizeComparable = (p) => {
|
|
2108
|
+
try {
|
|
2109
|
+
if (fs.existsSync(p)) return fs.realpathSync.native(p).replace(/\\/g, "/").toLowerCase();
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
logger.debug(`[luals] Failed to resolve native realpath for comparison: ${err instanceof Error ? err.message : String(err)}`);
|
|
2112
|
+
}
|
|
2113
|
+
return path.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
2114
|
+
};
|
|
2115
|
+
const normTarget = normalizeComparable(targetFileOnly);
|
|
2116
|
+
for (const [rawUri, diags] of Object.entries(diagnostics)) if (normalizeComparable(fileUriToPath(rawUri)) === normTarget) filtered[rawUri] = diags;
|
|
2117
|
+
diagnostics = filtered;
|
|
2118
|
+
}
|
|
2119
|
+
let totalProblems = 0;
|
|
2120
|
+
let totalErrors = 0;
|
|
2121
|
+
let totalWarnings = 0;
|
|
2122
|
+
let problemFiles = 0;
|
|
2123
|
+
for (const [_, diags] of Object.entries(diagnostics)) if (diags.length > 0) {
|
|
2124
|
+
problemFiles += 1;
|
|
2125
|
+
totalProblems += diags.length;
|
|
2126
|
+
for (const d of diags) if (d.severity === 1) totalErrors += 1;
|
|
2127
|
+
else if (d.severity === 2) totalWarnings += 1;
|
|
2128
|
+
}
|
|
2129
|
+
const passed = totalProblems === 0;
|
|
2130
|
+
const filesChecked = countCheckedFiles(targetPath, configPath);
|
|
2131
|
+
return {
|
|
2132
|
+
passed,
|
|
2133
|
+
totalProblems,
|
|
2134
|
+
totalErrors,
|
|
2135
|
+
totalWarnings,
|
|
2136
|
+
totalFiles: passed ? filesChecked : problemFiles,
|
|
2137
|
+
totalFilesChecked: filesChecked,
|
|
2138
|
+
diagnostics
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
//#endregion
|
|
1584
2142
|
//#region src/reporter.ts
|
|
1585
2143
|
const SEVERITY_NAMES = {
|
|
1586
2144
|
1: "Error",
|
|
@@ -1661,7 +2219,9 @@ function formatPretty(result, cwd = process.cwd(), useColor = shouldEnableColor(
|
|
|
1661
2219
|
let fileContent = [];
|
|
1662
2220
|
try {
|
|
1663
2221
|
if (fs.existsSync(filePath)) fileContent = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
1664
|
-
} catch {
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
logger.debug(`Failed to read file snippet for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2224
|
+
}
|
|
1665
2225
|
for (const d of diags) {
|
|
1666
2226
|
const line = d.range.start.line + 1;
|
|
1667
2227
|
const col = d.range.start.character + 1;
|
|
@@ -4714,9 +5274,43 @@ function readAnnotationsMetadata(cacheDir = getAnnotationsCacheDir()) {
|
|
|
4714
5274
|
const content = fs.readFileSync(metaPath, "utf-8");
|
|
4715
5275
|
const parsed = JSON.parse(content);
|
|
4716
5276
|
if (typeof parsed?.commitId === "string" && typeof parsed?.lastChecked === "string") return parsed;
|
|
4717
|
-
|
|
5277
|
+
try {
|
|
5278
|
+
fs.unlinkSync(metaPath);
|
|
5279
|
+
logger.warn(`[annotations] Stale or invalid annotations metadata at ${metaPath} purged.`);
|
|
5280
|
+
} catch (unlinkErr) {
|
|
5281
|
+
logger.debug(`[annotations] Failed to unlink invalid metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
5282
|
+
}
|
|
5283
|
+
} catch (err) {
|
|
5284
|
+
logger.debug(`Failed to parse annotations metadata: ${err instanceof Error ? err.message : String(err)}`);
|
|
5285
|
+
try {
|
|
5286
|
+
fs.unlinkSync(metaPath);
|
|
5287
|
+
logger.warn(`[annotations] Corrupted annotations metadata at ${metaPath} purged.`);
|
|
5288
|
+
} catch (unlinkErr) {
|
|
5289
|
+
logger.debug(`[annotations] Failed to unlink corrupted metadata: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
4718
5292
|
return null;
|
|
4719
5293
|
}
|
|
5294
|
+
/**
|
|
5295
|
+
* Verifies that an annotations file exists, is a regular file, is of non-trivial size (>= 1000 bytes),
|
|
5296
|
+
* and begins with valid Lua comments or nanos world definitions.
|
|
5297
|
+
*/
|
|
5298
|
+
function isAnnotationsValid(filePath) {
|
|
5299
|
+
if (!fs.existsSync(filePath)) return false;
|
|
5300
|
+
try {
|
|
5301
|
+
const stat = fs.statSync(filePath);
|
|
5302
|
+
if (!stat.isFile() || stat.size < 1e3) return false;
|
|
5303
|
+
const fd = fs.openSync(filePath, "r");
|
|
5304
|
+
const buffer = Buffer.alloc(256);
|
|
5305
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 256, 0);
|
|
5306
|
+
fs.closeSync(fd);
|
|
5307
|
+
const header = buffer.toString("utf-8", 0, bytesRead).trimStart();
|
|
5308
|
+
return header.startsWith("--") || header.includes("nanos world");
|
|
5309
|
+
} catch (err) {
|
|
5310
|
+
logger.debug(`[annotations] Annotation validation failed for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5311
|
+
return false;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
4720
5314
|
async function fetchLatestCommitId() {
|
|
4721
5315
|
try {
|
|
4722
5316
|
const headers = { "User-Agent": "nanos-lint" };
|
|
@@ -4729,7 +5323,9 @@ async function fetchLatestCommitId() {
|
|
|
4729
5323
|
const data = await res.json();
|
|
4730
5324
|
if (typeof data.sha === "string" && /^[0-9a-fA-F]{7,40}$/.test(data.sha)) return data.sha;
|
|
4731
5325
|
}
|
|
4732
|
-
} catch {
|
|
5326
|
+
} catch (err) {
|
|
5327
|
+
logger.warn(`Failed to resolve latest annotations commit from GitHub: ${err instanceof Error ? err.message : String(err)}`);
|
|
5328
|
+
}
|
|
4733
5329
|
return null;
|
|
4734
5330
|
}
|
|
4735
5331
|
async function fetchRawAnnotationsContent() {
|
|
@@ -4737,9 +5333,9 @@ async function fetchRawAnnotationsContent() {
|
|
|
4737
5333
|
headers: { "User-Agent": "nanos-lint" },
|
|
4738
5334
|
signal: AbortSignal.timeout(15e3)
|
|
4739
5335
|
});
|
|
4740
|
-
if (!res.ok) throw new
|
|
5336
|
+
if (!res.ok) throw new AnnotationsError(`Failed to download annotations.lua: ${res.status} ${res.statusText}`, "ERR_ANNOTATIONS_DOWNLOAD", "Verify your internet connection and that GitHub raw endpoints are accessible.");
|
|
4741
5337
|
const text = await res.text();
|
|
4742
|
-
if (!text || text.length < 1e3) throw new
|
|
5338
|
+
if (!text || text.length < 1e3) throw new AnnotationsError("Downloaded annotations.lua appears truncated or invalid", "ERR_ANNOTATIONS_INVALID", "Retry downloading or pass a local annotations file via --annotations <path>.");
|
|
4743
5339
|
return text;
|
|
4744
5340
|
}
|
|
4745
5341
|
async function copyFileWithRetry(src, dest, maxRetries = 10) {
|
|
@@ -4774,7 +5370,9 @@ function updateLastCheckedDate(commitId, cacheDir = getAnnotationsCacheDir()) {
|
|
|
4774
5370
|
} finally {
|
|
4775
5371
|
try {
|
|
4776
5372
|
if (fs.existsSync(tempMetaPath)) fs.unlinkSync(tempMetaPath);
|
|
4777
|
-
} catch {
|
|
5373
|
+
} catch (err) {
|
|
5374
|
+
logger.warn(`Failed to clean up temporary metadata file ${tempMetaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5375
|
+
}
|
|
4778
5376
|
}
|
|
4779
5377
|
return metadata;
|
|
4780
5378
|
}
|
|
@@ -4792,8 +5390,8 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
|
|
|
4792
5390
|
try {
|
|
4793
5391
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
4794
5392
|
if (!options?.quiet) {
|
|
4795
|
-
if (commitId && commitId !== "unknown")
|
|
4796
|
-
else
|
|
5393
|
+
if (commitId && commitId !== "unknown") logger.info(`[annotations] Downloading nanos world API annotations (${commitId.slice(0, 7)})...`);
|
|
5394
|
+
else logger.info("[annotations] Downloading nanos world API annotations...");
|
|
4797
5395
|
}
|
|
4798
5396
|
const content = await fetchRawAnnotationsContent();
|
|
4799
5397
|
const tempAnnotationsPath = path.join(tempDir, ANNOTATIONS_FILENAME);
|
|
@@ -4819,8 +5417,8 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
|
|
|
4819
5417
|
await copyFileWithRetry(tempAnnotationsPath, finalAnnotationsPath);
|
|
4820
5418
|
await copyFileWithRetry(tempMetaPath, finalMetaPath);
|
|
4821
5419
|
if (!options?.quiet) {
|
|
4822
|
-
if (commitId && commitId !== "unknown")
|
|
4823
|
-
else
|
|
5420
|
+
if (commitId && commitId !== "unknown") logger.info(`[annotations] Updated annotations.lua to commit ${commitId.slice(0, 7)}.`);
|
|
5421
|
+
else logger.info("[annotations] Updated annotations.lua.");
|
|
4824
5422
|
}
|
|
4825
5423
|
return finalAnnotationsPath;
|
|
4826
5424
|
} catch (err) {
|
|
@@ -4829,7 +5427,9 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
|
|
|
4829
5427
|
const backupMeta = path.join(backupDir, METADATA_FILENAME);
|
|
4830
5428
|
if (fs.existsSync(backupAnnotations)) await copyFileWithRetry(backupAnnotations, finalAnnotationsPath);
|
|
4831
5429
|
if (fs.existsSync(backupMeta)) await copyFileWithRetry(backupMeta, finalMetaPath);
|
|
4832
|
-
} catch {
|
|
5430
|
+
} catch (rollbackErr) {
|
|
5431
|
+
logger.error(`Failed to restore annotations from backup during rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`);
|
|
5432
|
+
}
|
|
4833
5433
|
throw err;
|
|
4834
5434
|
} finally {
|
|
4835
5435
|
try {
|
|
@@ -4841,7 +5441,9 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
|
|
|
4841
5441
|
recursive: true,
|
|
4842
5442
|
force: true
|
|
4843
5443
|
});
|
|
4844
|
-
} catch {
|
|
5444
|
+
} catch (err) {
|
|
5445
|
+
logger.warn(`Failed to clean up annotations temporary or backup directory: ${err instanceof Error ? err.message : String(err)}`);
|
|
5446
|
+
}
|
|
4845
5447
|
}
|
|
4846
5448
|
}
|
|
4847
5449
|
/**
|
|
@@ -4855,39 +5457,53 @@ async function downloadAndCacheAnnotations(commitId, cacheDir = getAnnotationsCa
|
|
|
4855
5457
|
async function resolveAnnotations(options = {}) {
|
|
4856
5458
|
if (options.customPath) {
|
|
4857
5459
|
const resolved = path.resolve(options.customPath);
|
|
4858
|
-
if (!fs.existsSync(resolved)) throw new
|
|
5460
|
+
if (!fs.existsSync(resolved)) throw new AnnotationsError(`Custom annotations file not found: ${resolved}`, "ERR_ANNOTATIONS_NOT_FOUND", "Verify that the path specified in --annotations exists and is accessible.");
|
|
4859
5461
|
return resolved;
|
|
4860
5462
|
}
|
|
4861
5463
|
const envPath = process.env.NANOS_ANNOTATIONS_PATH || process.env.NANOS_ANNOTATIONS;
|
|
4862
5464
|
if (envPath) {
|
|
4863
5465
|
const resolved = path.resolve(envPath);
|
|
4864
|
-
if (!fs.existsSync(resolved)) throw new
|
|
5466
|
+
if (!fs.existsSync(resolved)) throw new AnnotationsError(`Annotations file specified in environment not found: ${resolved}`, "ERR_ANNOTATIONS_ENV_NOT_FOUND", "Verify that the path specified in NANOS_ANNOTATIONS_PATH or NANOS_ANNOTATIONS exists.");
|
|
4865
5467
|
return resolved;
|
|
4866
5468
|
}
|
|
4867
5469
|
const bundled = path.join(getPackageRoot(), "annotations.lua");
|
|
4868
|
-
if (fs.existsSync(bundled)) return bundled;
|
|
5470
|
+
if (fs.existsSync(bundled) && isAnnotationsValid(bundled)) return bundled;
|
|
4869
5471
|
const cacheDir = options.cacheDir ?? getAnnotationsCacheDir();
|
|
4870
5472
|
const cachedAnnotationsFile = path.join(cacheDir, ANNOTATIONS_FILENAME);
|
|
4871
5473
|
const metadata = readAnnotationsMetadata(cacheDir);
|
|
4872
5474
|
const { dateStr } = getTodayDateString();
|
|
4873
|
-
if (
|
|
5475
|
+
if (fs.existsSync(cachedAnnotationsFile) && !isAnnotationsValid(cachedAnnotationsFile)) {
|
|
5476
|
+
logger.warn("[annotations] Cached annotations.lua is corrupted or empty. Purging and refreshing...");
|
|
5477
|
+
try {
|
|
5478
|
+
fs.unlinkSync(cachedAnnotationsFile);
|
|
5479
|
+
} catch (err) {
|
|
5480
|
+
logger.warn(`[annotations] Failed to remove corrupted cached annotations: ${err instanceof Error ? err.message : String(err)}`);
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
if (metadata && metadata.lastChecked === dateStr && fs.existsSync(cachedAnnotationsFile) && isAnnotationsValid(cachedAnnotationsFile)) return cachedAnnotationsFile;
|
|
4874
5484
|
const latestCommitId = await fetchLatestCommitId();
|
|
4875
5485
|
if (latestCommitId) {
|
|
4876
|
-
if (metadata && metadata.commitId === latestCommitId && fs.existsSync(cachedAnnotationsFile)) {
|
|
5486
|
+
if (metadata && metadata.commitId === latestCommitId && fs.existsSync(cachedAnnotationsFile) && isAnnotationsValid(cachedAnnotationsFile)) {
|
|
4877
5487
|
updateLastCheckedDate(latestCommitId, cacheDir);
|
|
4878
5488
|
return cachedAnnotationsFile;
|
|
4879
5489
|
}
|
|
4880
5490
|
return await downloadAndCacheAnnotations(latestCommitId, cacheDir, options);
|
|
4881
5491
|
}
|
|
4882
|
-
if (fs.existsSync(cachedAnnotationsFile)) {
|
|
5492
|
+
if (fs.existsSync(cachedAnnotationsFile) && isAnnotationsValid(cachedAnnotationsFile)) {
|
|
4883
5493
|
try {
|
|
4884
5494
|
updateLastCheckedDate(metadata?.commitId || "unknown", cacheDir);
|
|
4885
|
-
} catch {
|
|
5495
|
+
} catch (err) {
|
|
5496
|
+
logger.warn(`Failed to update lastChecked date for cached annotations: ${err instanceof Error ? err.message : String(err)}`);
|
|
5497
|
+
}
|
|
4886
5498
|
return cachedAnnotationsFile;
|
|
4887
5499
|
}
|
|
4888
5500
|
try {
|
|
4889
5501
|
return await downloadAndCacheAnnotations("unknown", cacheDir, options);
|
|
4890
5502
|
} catch (err) {
|
|
5503
|
+
if (fs.existsSync(bundled) && isAnnotationsValid(bundled)) {
|
|
5504
|
+
logger.info("[annotations] Network offline and cache unavailable. Falling back to bundled annotations.");
|
|
5505
|
+
return bundled;
|
|
5506
|
+
}
|
|
4891
5507
|
if (Boolean(err && typeof err === "object" && "code" in err && typeof err.code === "string" && [
|
|
4892
5508
|
"EACCES",
|
|
4893
5509
|
"EPERM",
|
|
@@ -4895,20 +5511,116 @@ async function resolveAnnotations(options = {}) {
|
|
|
4895
5511
|
"EROFS",
|
|
4896
5512
|
"EEXIST",
|
|
4897
5513
|
"ENOENT"
|
|
4898
|
-
].includes(err.code)) || err instanceof Error && /permission denied|read-only|no space left/i.test(err.message)) throw new
|
|
4899
|
-
throw new
|
|
5514
|
+
].includes(err.code)) || err instanceof Error && /permission denied|read-only|no space left/i.test(err.message)) throw new AnnotationsError(`Failed to resolve nanos world API annotations due to a filesystem error: ${err instanceof Error ? err.message : String(err)}. Please check directory permissions or pass a custom file with --annotations.`, "ERR_ANNOTATIONS_FS", "Check directory permissions for the cache directory or specify --annotations <path>.", { cause: err });
|
|
5515
|
+
throw new AnnotationsError(`Failed to resolve nanos world API annotations. Please check your network connection or pass a custom file with --annotations. (${err instanceof Error ? err.message : String(err)})`, "ERR_ANNOTATIONS_NETWORK", "Check your network connection, run 'nanos-lint warmup' when online, or specify --annotations <path>.", { cause: err });
|
|
4900
5516
|
}
|
|
4901
5517
|
}
|
|
4902
5518
|
//#endregion
|
|
5519
|
+
//#region src/cache-status.ts
|
|
5520
|
+
function getCacheStatus(baseCacheDir = systemPaths.cache) {
|
|
5521
|
+
const totalSize = getDirectorySize(baseCacheDir);
|
|
5522
|
+
const totalSizeFormatted = formatBytes(totalSize);
|
|
5523
|
+
const lualsBaseDir = path.join(baseCacheDir, "luals");
|
|
5524
|
+
const lualsMeta = readLuaLSMetadata(lualsBaseDir);
|
|
5525
|
+
const candidateVersions = listCachedLuaLSVersionDirs(lualsBaseDir);
|
|
5526
|
+
const versionInfos = [];
|
|
5527
|
+
for (const ver of candidateVersions) {
|
|
5528
|
+
const versionDir = getCacheDir(ver, lualsBaseDir);
|
|
5529
|
+
const marker = path.join(versionDir, ".complete");
|
|
5530
|
+
const info = getPlatformInfo(ver);
|
|
5531
|
+
const binPath = path.join(versionDir, info.binaryRelativePath);
|
|
5532
|
+
let isValid = false;
|
|
5533
|
+
if (fs.existsSync(marker) && fs.existsSync(binPath)) try {
|
|
5534
|
+
isValid = fs.readFileSync(marker, "utf-8").trim() === ver && isBinaryValid(binPath);
|
|
5535
|
+
} catch {
|
|
5536
|
+
isValid = false;
|
|
5537
|
+
}
|
|
5538
|
+
const dirSize = getDirectorySize(versionDir);
|
|
5539
|
+
versionInfos.push({
|
|
5540
|
+
version: ver,
|
|
5541
|
+
status: isValid ? "valid" : "corrupted",
|
|
5542
|
+
size: dirSize,
|
|
5543
|
+
sizeFormatted: formatBytes(dirSize),
|
|
5544
|
+
path: versionDir
|
|
5545
|
+
});
|
|
5546
|
+
}
|
|
5547
|
+
const lualsStatus = {
|
|
5548
|
+
weeklyCheck: lualsMeta?.lastCheckedWeek ?? null,
|
|
5549
|
+
lastCheckedDate: lualsMeta?.lastCheckedDate ?? null,
|
|
5550
|
+
targetVersion: lualsMeta?.latestVersion ?? null,
|
|
5551
|
+
versions: versionInfos
|
|
5552
|
+
};
|
|
5553
|
+
const annotationsDir = path.join(baseCacheDir, "annotations");
|
|
5554
|
+
const annotationsFile = path.join(annotationsDir, "annotations.lua");
|
|
5555
|
+
const annMeta = readAnnotationsMetadata(annotationsDir);
|
|
5556
|
+
let annStatus = "missing";
|
|
5557
|
+
let annSize = 0;
|
|
5558
|
+
if (fs.existsSync(annotationsFile)) {
|
|
5559
|
+
annSize = getDirectorySize(annotationsFile);
|
|
5560
|
+
annStatus = isAnnotationsValid(annotationsFile) ? "valid" : "corrupted";
|
|
5561
|
+
}
|
|
5562
|
+
return {
|
|
5563
|
+
cacheDirectory: baseCacheDir,
|
|
5564
|
+
totalSize,
|
|
5565
|
+
totalSizeFormatted,
|
|
5566
|
+
luals: lualsStatus,
|
|
5567
|
+
annotations: {
|
|
5568
|
+
status: annStatus,
|
|
5569
|
+
size: annSize,
|
|
5570
|
+
sizeFormatted: formatBytes(annSize),
|
|
5571
|
+
path: annotationsFile,
|
|
5572
|
+
commitId: annMeta?.commitId ?? null,
|
|
5573
|
+
lastChecked: annMeta?.lastChecked ?? null
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
5576
|
+
}
|
|
5577
|
+
function formatCacheStatusPretty(report) {
|
|
5578
|
+
const lines = [];
|
|
5579
|
+
lines.push("nanos-lint Cache Status");
|
|
5580
|
+
lines.push("");
|
|
5581
|
+
lines.push(`Cache Directory: ${report.cacheDirectory}`);
|
|
5582
|
+
lines.push(`Total Disk Usage: ${report.totalSizeFormatted}`);
|
|
5583
|
+
lines.push("");
|
|
5584
|
+
lines.push("Lua Language Server (LuaLS):");
|
|
5585
|
+
const weeklyStr = report.luals.weeklyCheck ? `${report.luals.weeklyCheck}${report.luals.lastCheckedDate ? ` (last checked: ${report.luals.lastCheckedDate})` : ""}` : "none";
|
|
5586
|
+
lines.push(` - Weekly Check: ${weeklyStr}`);
|
|
5587
|
+
const targetVer = report.luals.targetVersion ?? "none";
|
|
5588
|
+
lines.push(` - Target Version: ${targetVer}`);
|
|
5589
|
+
if (report.luals.versions.length === 0) lines.push(" - Cached Copies: none");
|
|
5590
|
+
else {
|
|
5591
|
+
lines.push(" - Cached Copies:");
|
|
5592
|
+
for (const v of report.luals.versions) lines.push(` * ${v.version} (status: ${v.status}, size: ${v.sizeFormatted})`);
|
|
5593
|
+
}
|
|
5594
|
+
lines.push("");
|
|
5595
|
+
lines.push("nanos world Annotations:");
|
|
5596
|
+
if (report.annotations.status === "missing") lines.push(" - Status: Not cached");
|
|
5597
|
+
else {
|
|
5598
|
+
const statusCap = report.annotations.status === "valid" ? "Valid" : "Corrupted";
|
|
5599
|
+
lines.push(` - Status: ${statusCap} (${report.annotations.sizeFormatted})`);
|
|
5600
|
+
if (report.annotations.commitId) {
|
|
5601
|
+
const sha = report.annotations.commitId;
|
|
5602
|
+
const shaDisplay = sha.length >= 7 && sha !== "unknown" ? `${sha.slice(0, 8)} (docgen-output)` : sha;
|
|
5603
|
+
lines.push(` - Commit SHA: ${shaDisplay}`);
|
|
5604
|
+
}
|
|
5605
|
+
if (report.annotations.lastChecked) lines.push(` - Last Checked: ${report.annotations.lastChecked}`);
|
|
5606
|
+
}
|
|
5607
|
+
return lines.join("\n");
|
|
5608
|
+
}
|
|
5609
|
+
//#endregion
|
|
4903
5610
|
//#region src/cli.ts
|
|
4904
5611
|
function getVersionString() {
|
|
4905
5612
|
const root = getPackageRoot();
|
|
4906
5613
|
try {
|
|
4907
5614
|
return `nanos-lint v${JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8")).version}`;
|
|
4908
|
-
} catch {
|
|
5615
|
+
} catch (err) {
|
|
5616
|
+
logger.debug(`Could not read version from package.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
4909
5617
|
return "nanos-lint v1.0.0";
|
|
4910
5618
|
}
|
|
4911
5619
|
}
|
|
5620
|
+
/** Writes command results to stdout unless the level is `silent`. */
|
|
5621
|
+
function writeOutput(message) {
|
|
5622
|
+
if (logger.isOutputEnabled()) console.log(message);
|
|
5623
|
+
}
|
|
4912
5624
|
function collectIgnorePatterns(val, prev) {
|
|
4913
5625
|
const parts = val.split(/[\r\n,]+/).map((s) => s.trim()).filter(Boolean);
|
|
4914
5626
|
return (prev ?? []).concat(parts);
|
|
@@ -4916,9 +5628,20 @@ function collectIgnorePatterns(val, prev) {
|
|
|
4916
5628
|
function createProgram(options) {
|
|
4917
5629
|
const setExitCode = options?.setExitCode ?? (() => {});
|
|
4918
5630
|
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").
|
|
5631
|
+
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([
|
|
5632
|
+
"error",
|
|
5633
|
+
"warn",
|
|
5634
|
+
"info",
|
|
5635
|
+
"debug",
|
|
5636
|
+
"silent"
|
|
5637
|
+
]).default(DEFAULT_LOG_LEVEL)).hook("preAction", (thisCommand, actionCommand) => {
|
|
5638
|
+
const target = actionCommand || thisCommand;
|
|
5639
|
+
const opts = target.optsWithGlobals ? target.optsWithGlobals() : target.opts();
|
|
5640
|
+
if (opts.quiet) logger.setLevel("error");
|
|
5641
|
+
else if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
|
|
5642
|
+
}).exitOverride().configureOutput({
|
|
4920
5643
|
writeOut: (str) => console.log(str.trimEnd()),
|
|
4921
|
-
writeErr: (str) =>
|
|
5644
|
+
writeErr: (str) => logger.error(str.trimEnd())
|
|
4922
5645
|
});
|
|
4923
5646
|
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
5647
|
"Error",
|
|
@@ -4929,7 +5652,15 @@ function createProgram(options) {
|
|
|
4929
5652
|
"pretty",
|
|
4930
5653
|
"json",
|
|
4931
5654
|
"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").
|
|
5655
|
+
])).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([
|
|
5656
|
+
"error",
|
|
5657
|
+
"warn",
|
|
5658
|
+
"info",
|
|
5659
|
+
"debug",
|
|
5660
|
+
"silent"
|
|
5661
|
+
])).option("--github", "Output in GitHub Actions format (shortcut for --format=github)").action(async (targetPath = ".", opts) => {
|
|
5662
|
+
if (opts.quiet) logger.setLevel("error");
|
|
5663
|
+
else if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
|
|
4933
5664
|
const format = opts.github ? "github" : opts.format || (process.env.GITHUB_ACTIONS ? "github" : "pretty");
|
|
4934
5665
|
const checkOptions = {
|
|
4935
5666
|
path: targetPath,
|
|
@@ -4943,7 +5674,7 @@ function createProgram(options) {
|
|
|
4943
5674
|
};
|
|
4944
5675
|
if (opts.config) {
|
|
4945
5676
|
const resolvedConfig = path.resolve(opts.config);
|
|
4946
|
-
if (!fs.existsSync(resolvedConfig)) throw new
|
|
5677
|
+
if (!fs.existsSync(resolvedConfig)) throw new ConfigError(`Configuration file not found: ${resolvedConfig}`, "ERR_CONFIG_NOT_FOUND", "Verify the path passed to --config exists and is readable.");
|
|
4947
5678
|
}
|
|
4948
5679
|
const annotationsPath = await resolveAnnotations({
|
|
4949
5680
|
customPath: opts.annotations,
|
|
@@ -4959,48 +5690,81 @@ function createProgram(options) {
|
|
|
4959
5690
|
} finally {
|
|
4960
5691
|
if (resolved.isTemp && fs.existsSync(resolved.configPath)) try {
|
|
4961
5692
|
fs.unlinkSync(resolved.configPath);
|
|
4962
|
-
} catch {
|
|
5693
|
+
} catch (err) {
|
|
5694
|
+
logger.warn(`Failed to clean up temporary config file ${resolved.configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5695
|
+
}
|
|
4963
5696
|
}
|
|
4964
5697
|
const output = formatReport(result, checkOptions.format, process.cwd());
|
|
4965
|
-
if (output)
|
|
5698
|
+
if (output) writeOutput(output);
|
|
4966
5699
|
if (!result.passed && checkOptions.failOnError) setExitCode(1);
|
|
4967
5700
|
else setExitCode(0);
|
|
4968
5701
|
});
|
|
4969
5702
|
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
5703
|
const annotationsPath = await resolveAnnotations({ customPath: opts.annotations });
|
|
4971
|
-
|
|
5704
|
+
writeOutput(`[init] Initialized nanos world LuaLS configuration: ${initWorkspace(path.resolve(targetPath), {
|
|
4972
5705
|
force: opts.force,
|
|
4973
5706
|
annotationsPath
|
|
5707
|
+
})}`);
|
|
5708
|
+
setExitCode(0);
|
|
5709
|
+
});
|
|
5710
|
+
program.command("warmup").alias("download").description("Pre-fetch and cache both LuaLS binary and annotations for offline execution").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).option("--annotations <path>", "Path to custom annotations.lua file").option("-q, --quiet", "Suppress download progress logging").action(async (opts) => {
|
|
5711
|
+
writeOutput(`[warmup] LuaLS binary ready: ${await resolveLuaLSBinary(opts?.lualsVersion || "latest", { quiet: opts?.quiet })}`);
|
|
5712
|
+
const annotationsPath = await resolveAnnotations({
|
|
5713
|
+
customPath: opts?.annotations,
|
|
5714
|
+
quiet: opts?.quiet
|
|
4974
5715
|
});
|
|
4975
|
-
|
|
5716
|
+
const meta = readAnnotationsMetadata();
|
|
5717
|
+
writeOutput(`[warmup] nanos world annotations ready: ${annotationsPath}${meta?.commitId && meta.commitId !== "unknown" ? ` (commit ${meta.commitId.slice(0, 7)})` : ""}`);
|
|
5718
|
+
writeOutput("[warmup] Cache pre-warmed successfully. Ready for offline execution.");
|
|
4976
5719
|
setExitCode(0);
|
|
4977
5720
|
});
|
|
4978
5721
|
program.command("download-luals [version]").description("Download and cache the LuaLS binary").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).action(async (version, opts) => {
|
|
4979
5722
|
const ver = version || opts?.lualsVersion || "latest";
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
console.log(`[luals] Ready at: ${bin}`);
|
|
5723
|
+
writeOutput(`[luals] Downloading LuaLS ${ver}...`);
|
|
5724
|
+
writeOutput(`[luals] Ready at: ${await resolveLuaLSBinary(ver)}`);
|
|
4983
5725
|
setExitCode(0);
|
|
4984
5726
|
});
|
|
4985
|
-
|
|
5727
|
+
const handleCleanCache = () => {
|
|
4986
5728
|
try {
|
|
4987
5729
|
const cleared = cleanCache();
|
|
4988
|
-
if (cleared)
|
|
4989
|
-
else
|
|
5730
|
+
if (cleared) writeOutput(`[cache] Cleared cache at: ${cleared}`);
|
|
5731
|
+
else writeOutput(`[cache] Cache is already empty (${systemPaths.cache})`);
|
|
4990
5732
|
setExitCode(0);
|
|
4991
5733
|
} catch (err) {
|
|
4992
|
-
|
|
5734
|
+
logger.error(`[cache] Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
4993
5735
|
setExitCode(1);
|
|
4994
5736
|
}
|
|
5737
|
+
};
|
|
5738
|
+
const handleCacheStatus = (opts) => {
|
|
5739
|
+
const report = getCacheStatus();
|
|
5740
|
+
if (opts?.json) writeOutput(JSON.stringify(report, null, 2));
|
|
5741
|
+
else writeOutput(formatCacheStatusPretty(report));
|
|
5742
|
+
setExitCode(0);
|
|
5743
|
+
};
|
|
5744
|
+
const cacheCmd = program.command("cache").description("Inspect or manage the nanos-lint cache").option("--json", "Output cache state in JSON format");
|
|
5745
|
+
cacheCmd.command("status", { isDefault: true }).alias("info").description("Show cache status, installed versions, and disk usage").option("--json", "Output cache state in JSON format").action((opts, cmd) => {
|
|
5746
|
+
const json = opts?.json || (cmd.parent ? Boolean(cmd.parent.opts()?.json) : false);
|
|
5747
|
+
handleCacheStatus({ json });
|
|
5748
|
+
});
|
|
5749
|
+
cacheCmd.command("clean").description("Clear the nanos-lint cache").action(() => {
|
|
5750
|
+
handleCleanCache();
|
|
5751
|
+
});
|
|
5752
|
+
program.command("cache-status").description("Show cache status, installed versions, and disk usage").option("--json", "Output cache state in JSON format").action((opts) => {
|
|
5753
|
+
handleCacheStatus(opts);
|
|
5754
|
+
});
|
|
5755
|
+
program.command("clean-cache").alias("clean").description("Clear the nanos-lint cache").action(() => {
|
|
5756
|
+
handleCleanCache();
|
|
4995
5757
|
});
|
|
4996
5758
|
program.command("version").description("Show version information").action(() => {
|
|
4997
|
-
|
|
5759
|
+
writeOutput(getVersionString());
|
|
4998
5760
|
setExitCode(0);
|
|
4999
5761
|
});
|
|
5000
5762
|
program.helpCommand("help [command]", "Show this help message");
|
|
5001
5763
|
program.addHelpText("after", `
|
|
5002
5764
|
Examples:
|
|
5003
5765
|
$ npx nanos-lint
|
|
5766
|
+
$ npx nanos-lint warmup
|
|
5767
|
+
$ npx nanos-lint cache status
|
|
5004
5768
|
$ npx nanos-lint check ./my-package
|
|
5005
5769
|
$ npx nanos-lint check . --checklevel=Error
|
|
5006
5770
|
$ npx nanos-lint check . --ignore "myfolder/hello-*.lua"
|
|
@@ -5010,6 +5774,17 @@ Examples:
|
|
|
5010
5774
|
return program;
|
|
5011
5775
|
}
|
|
5012
5776
|
async function runCLI(args = process.argv.slice(2)) {
|
|
5777
|
+
for (let i = 0; i < args.length; i++) {
|
|
5778
|
+
const arg = args[i];
|
|
5779
|
+
if (!arg) continue;
|
|
5780
|
+
if (arg === "-l" || arg === "--log-level") {
|
|
5781
|
+
const nextArg = args[i + 1];
|
|
5782
|
+
if (nextArg && isValidLogLevel(nextArg)) logger.setLevel(nextArg);
|
|
5783
|
+
} else if (arg.startsWith("--log-level=")) {
|
|
5784
|
+
const val = arg.split("=")[1];
|
|
5785
|
+
if (val && isValidLogLevel(val)) logger.setLevel(val);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5013
5788
|
let exitCode = 0;
|
|
5014
5789
|
const program = createProgram({ setExitCode: (code) => {
|
|
5015
5790
|
exitCode = code;
|
|
@@ -5019,9 +5794,18 @@ async function runCLI(args = process.argv.slice(2)) {
|
|
|
5019
5794
|
return exitCode;
|
|
5020
5795
|
} catch (err) {
|
|
5021
5796
|
if (err instanceof CommanderError) return err.exitCode;
|
|
5797
|
+
if (err instanceof NanosLintError) {
|
|
5798
|
+
logger.error(`error: ${err.message}`);
|
|
5799
|
+
if (err.remedy) logger.error(`hint: ${err.remedy}`);
|
|
5800
|
+
if ((logger.getLevel() === "debug" || process.env.DEBUG) && err.stack) {
|
|
5801
|
+
logger.error(err.stack);
|
|
5802
|
+
if (err.cause) logger.error(`cause: ${err.cause instanceof Error ? err.cause.stack || err.cause.message : String(err.cause)}`);
|
|
5803
|
+
}
|
|
5804
|
+
return 1;
|
|
5805
|
+
}
|
|
5022
5806
|
const message = err instanceof Error ? err.message : String(err);
|
|
5023
|
-
|
|
5024
|
-
if (process.env.DEBUG && err instanceof Error && err.stack)
|
|
5807
|
+
logger.error(`error: ${message}`);
|
|
5808
|
+
if ((logger.getLevel() === "debug" || process.env.DEBUG) && err instanceof Error && err.stack) logger.error(err.stack);
|
|
5025
5809
|
return 1;
|
|
5026
5810
|
}
|
|
5027
5811
|
}
|
|
@@ -5031,7 +5815,8 @@ function isDirectExecution(importMetaUrl = import.meta.url, argv1 = process.argv
|
|
|
5031
5815
|
try {
|
|
5032
5816
|
if (urlStr.startsWith("file:")) return fileURLToPath(urlStr);
|
|
5033
5817
|
return urlStr;
|
|
5034
|
-
} catch {
|
|
5818
|
+
} catch (err) {
|
|
5819
|
+
logger.debug(`[cli] Failed to convert URL "${urlStr}" using fileURLToPath: ${err instanceof Error ? err.message : String(err)}`);
|
|
5035
5820
|
return urlStr.replace(/^file:\/\/\/?/, "");
|
|
5036
5821
|
}
|
|
5037
5822
|
};
|
|
@@ -5043,7 +5828,8 @@ function isDirectExecution(importMetaUrl = import.meta.url, argv1 = process.argv
|
|
|
5043
5828
|
const cliJsPath = path.join(moduleDir, "cli.js").toLowerCase();
|
|
5044
5829
|
const cliTsPath = path.join(moduleDir, "cli.ts").toLowerCase();
|
|
5045
5830
|
if (scriptPath === cliJsPath || scriptPath === cliTsPath) return true;
|
|
5046
|
-
} catch {
|
|
5831
|
+
} catch (err) {
|
|
5832
|
+
logger.debug(`[cli] Failed to resolve realpath for entry detection: ${err instanceof Error ? err.message : String(err)}`);
|
|
5047
5833
|
const normArgv = path.resolve(argv1).toLowerCase();
|
|
5048
5834
|
const normMeta = toPath(importMetaUrl).toLowerCase();
|
|
5049
5835
|
if (normArgv === normMeta) return true;
|
|
@@ -5055,10 +5841,10 @@ function isDirectExecution(importMetaUrl = import.meta.url, argv1 = process.argv
|
|
|
5055
5841
|
if (isDirectExecution()) runCLI().then((code) => {
|
|
5056
5842
|
process.exit(code);
|
|
5057
5843
|
}).catch((err) => {
|
|
5058
|
-
|
|
5844
|
+
logger.error(err);
|
|
5059
5845
|
process.exit(1);
|
|
5060
5846
|
});
|
|
5061
5847
|
//#endregion
|
|
5062
|
-
export {
|
|
5848
|
+
export { fetchLatestLuaLSVersionFromGitHub as $, pluralize as A, logger as At, getBaseLuaLSCacheDir as B, updateLastCheckedDate as C, systemPaths as Ct, formatReport as D, LOG_LEVEL_PRIORITY as Dt, formatProblemSummary as E, LOG_LEVELS as Et, downloadAndExtractLuaLS as F, listCachedLuaLSVersionDirs as G, getIsoWeek as H, escapePowerShellSingleQuote as I, writeLuaLSMetadata as J, listCachedLuaLSVersions as K, LUALS_METADATA_FILENAME as L, resolveLuaLSBinary as M, runLuaLSCheck as N, formatSeverityBadge as O, Logger as Ot, countCheckedFiles as P, FALLBACK_LUALS_VERSION as Q, cleanupOldCachedLuaLSVersions as R, resolveAnnotations as S, getDirectorySize as St, formatPretty as T, DEFAULT_LOG_LEVEL as Tt, getLegacyCacheDir as U, getCacheDir as V, getLuaLSMetadataPath as W, getPlatformInfo as X, isBinaryValid as Y, DEFAULT_LUALS_VERSION as Z, getAnnotationsMetadataFilePath as _, ConfigError as _t, formatCacheStatusPretty as a, getDefaultTemplatePath as at, isAnnotationsValid as b, cleanCache as bt, DOCGEN_BRANCH as c, initWorkspace as ct, METADATA_FILENAME as d, parseJsonc as dt, resolveLatestLuaLSVersion as et, RAW_ANNOTATIONS_URL as f, resolveWorkspaceConfig as ft, getAnnotationsCacheDir as g, CacheError as gt, fetchRawAnnotationsContent as h, AnnotationsError as ht, runCLI as i, getDefaultAnnotationsPath as it, shouldEnableColor as j, parseLogLevel as jt, getColors as k, isValidLogLevel as kt, DOCGEN_REPO as l, loadConfigFile as lt, fetchLatestCommitId as m, stripTrailingSlashes as mt, createProgram as n, sanitizeLuaLSVersion as nt, getCacheStatus as o, getDefinitionsDir as ot, downloadAndCacheAnnotations as p, stripJsonComments as pt, readLuaLSMetadata as q, isDirectExecution as r, VALID_LUALS_DIAGNOSTIC_CODES as rt, ANNOTATIONS_FILENAME as s, getPackageRoot as st, collectIgnorePatterns as t, resolveLuaLSVersion as tt, GITHUB_COMMITS_API as u, mergeConfigs as ut, getCachedAnnotationsFilePath as v, LuaLSError as vt, formatGitHubAnnotations as w, fileUriToPath as wt, readAnnotationsMetadata as x, formatBytes as xt, getTodayDateString as y, NanosLintError as yt, findExistingLuaLSDir as z };
|
|
5063
5849
|
|
|
5064
|
-
//# sourceMappingURL=cli-
|
|
5850
|
+
//# sourceMappingURL=cli-DGez8MSI.js.map
|