nanos-lint 3.0.1 → 3.2.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.
@@ -41,6 +41,7 @@ function parseLogLevel(raw) {
41
41
  }
42
42
  var Logger = class {
43
43
  level = DEFAULT_LOG_LEVEL;
44
+ diagnosticStream = "stdout";
44
45
  constructor(initialLevel) {
45
46
  if (initialLevel && isValidLogLevel(initialLevel)) this.level = initialLevel;
46
47
  else {
@@ -56,6 +57,17 @@ var Logger = class {
56
57
  getLevel() {
57
58
  return this.level;
58
59
  }
60
+ /**
61
+ * Selects the stream used by `info`/`debug`. Machine-readable formats switch this to
62
+ * `stderr`, so a report on stdout is never interleaved with diagnostic lines.
63
+ */
64
+ setDiagnosticStream(stream) {
65
+ this.diagnosticStream = stream;
66
+ }
67
+ /** Returns the stream used by `info`/`debug`. */
68
+ getDiagnosticStream() {
69
+ return this.diagnosticStream;
70
+ }
59
71
  /** Checks if a message at the specified level should be emitted. */
60
72
  isEnabledFor(level) {
61
73
  return LOG_LEVEL_PRIORITY[this.level] >= LOG_LEVEL_PRIORITY[level];
@@ -72,13 +84,22 @@ var Logger = class {
72
84
  warn(...args) {
73
85
  if (this.isEnabledFor("warn")) console.warn(...args);
74
86
  }
75
- /** Logs an informational message to stdout. */
87
+ /** Logs an informational message to stdout, or to stderr when stdout carries a report. */
76
88
  info(...args) {
77
- if (this.isEnabledFor("info")) console.log(...args);
89
+ if (this.isEnabledFor("info")) this.emitDiagnostic(args, "log");
78
90
  }
79
- /** Logs a debug-level diagnostic message to stdout. */
91
+ /** Logs a debug-level diagnostic message to stdout, or to stderr when stdout carries a report. */
80
92
  debug(...args) {
81
- if (this.isEnabledFor("debug")) console.debug(...args);
93
+ if (this.isEnabledFor("debug")) this.emitDiagnostic(args, "debug");
94
+ }
95
+ /** Writes an `info`/`debug` diagnostic on the configured stream. */
96
+ emitDiagnostic(args, stdoutMethod) {
97
+ if (this.diagnosticStream === "stderr") {
98
+ console.error(...args);
99
+ return;
100
+ }
101
+ if (stdoutMethod === "debug") console.debug(...args);
102
+ else console.log(...args);
82
103
  }
83
104
  };
84
105
  const logger = new Logger();
@@ -1186,10 +1207,12 @@ function mergeConfigs(base, override = {}, definitionsPath = getDefaultAnnotatio
1186
1207
  const normalizedDefPath = resolvedPath.split(path.sep).join("/");
1187
1208
  const baseLibraries = base.workspace?.library ?? [];
1188
1209
  const overrideLibraries = override.workspace?.library ?? [];
1210
+ const depLibraries = (options?.dependencyLibraries ?? []).map((p) => p.replace(/\\/g, "/"));
1189
1211
  const librarySet = /* @__PURE__ */ new Set([
1190
1212
  normalizedDefPath,
1191
1213
  ...baseLibraries,
1192
- ...overrideLibraries
1214
+ ...overrideLibraries,
1215
+ ...depLibraries
1193
1216
  ]);
1194
1217
  const baseGlobals = base.diagnostics?.globals ?? [];
1195
1218
  const overrideGlobals = override.diagnostics?.globals ?? [];
@@ -1328,7 +1351,10 @@ function buildWorkspaceConfig(workspacePath, customConfigPath, options) {
1328
1351
  }
1329
1352
  cliIgnore = expanded;
1330
1353
  }
1331
- const merged = mergeConfigs(defaultTemplate, userConfig, activeAnnotationsPath, { cliIgnore });
1354
+ const merged = mergeConfigs(defaultTemplate, userConfig, activeAnnotationsPath, {
1355
+ cliIgnore,
1356
+ dependencyLibraries: options?.dependencyLibraries
1357
+ });
1332
1358
  if (!hasCliIgnore) {
1333
1359
  const resolvedTarget = path.resolve(workspacePath);
1334
1360
  if (fs.existsSync(path.join(resolvedTarget, "main.lua")) && (fs.existsSync(path.join(resolvedTarget, "bin", "lua-language-server.exe")) || fs.existsSync(path.join(resolvedTarget, "bin", "lua-language-server")))) {
@@ -1343,6 +1369,11 @@ function buildWorkspaceConfig(workspacePath, customConfigPath, options) {
1343
1369
  ])];
1344
1370
  }
1345
1371
  }
1372
+ if (options?.unrequestedExclusions && options.unrequestedExclusions.length > 0) {
1373
+ merged.files = merged.files ?? {};
1374
+ const existingExclude = merged.files.exclude ?? [];
1375
+ merged.files.exclude = [.../* @__PURE__ */ new Set([...existingExclude, ...options.unrequestedExclusions])];
1376
+ }
1346
1377
  return merged;
1347
1378
  }
1348
1379
  /**
@@ -5046,6 +5077,294 @@ async function writeAtomicFile(targetPath, data, options = {}) {
5046
5077
  }
5047
5078
  }
5048
5079
  //#endregion
5080
+ //#region src/terminal-progress.ts
5081
+ /** Minimum delay between two terminal redraws (about ten frames per second). */
5082
+ const PROGRESS_THROTTLE_MS = 100;
5083
+ /** Braille frames cycled while an indeterminate operation runs. */
5084
+ const SPINNER_FRAMES = [
5085
+ "⠋",
5086
+ "⠙",
5087
+ "⠹",
5088
+ "⠸",
5089
+ "⠼",
5090
+ "⠴",
5091
+ "⠦",
5092
+ "⠧",
5093
+ "⠇",
5094
+ "⠏"
5095
+ ];
5096
+ /** Fractions of a transfer logged as milestones when no terminal can be redrawn. */
5097
+ const PROGRESS_MILESTONES = [
5098
+ .25,
5099
+ .5,
5100
+ .75
5101
+ ];
5102
+ const CURSOR_HIDE = "\x1B[?25l";
5103
+ const CURSOR_SHOW = "\x1B[?25h";
5104
+ const CLEAR_LINE = "\r\x1B[2K";
5105
+ const FRAME_TAIL = " ";
5106
+ const DEFAULT_COLUMNS = 80;
5107
+ const MIN_BAR_WIDTH = 8;
5108
+ const MAX_BAR_WIDTH = 28;
5109
+ const TRUNCATION = "...";
5110
+ let progressMode = "auto";
5111
+ let cursorHidden = false;
5112
+ let guardsInstalled = false;
5113
+ let activeDynamicRenders = 0;
5114
+ /** Sets the redraw policy used by every progress display created afterwards. */
5115
+ function setProgressMode(mode) {
5116
+ progressMode = mode;
5117
+ }
5118
+ /** Returns the current redraw policy. */
5119
+ function getProgressMode() {
5120
+ return progressMode;
5121
+ }
5122
+ /** Returns whether an environment value is set to anything but an explicit false. */
5123
+ function isEnvFlagSet(value) {
5124
+ if (value === void 0) return false;
5125
+ const normalized = value.trim().toLowerCase();
5126
+ return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no" && normalized !== "off";
5127
+ }
5128
+ /**
5129
+ * Returns whether dynamic redrawing is allowed for the given environment and terminal.
5130
+ * Non-interactive environments get single-line milestones instead of carriage returns.
5131
+ */
5132
+ function isProgressInteractive(env = process.env, isTTY = process.stderr.isTTY) {
5133
+ if (progressMode === "off" || !logger.isOutputEnabled() || !isTTY) return false;
5134
+ if ((env.TERM ?? "").trim().toLowerCase() === "dumb") return false;
5135
+ return !(isEnvFlagSet(env.CI) || isEnvFlagSet(env.NO_COLOR) || isEnvFlagSet(env.NANOS_NO_PROGRESS));
5136
+ }
5137
+ /** Writes a control sequence straight to file descriptor 2, tolerating a closed stderr. */
5138
+ function writeToStderr(sequence) {
5139
+ try {
5140
+ fs.writeSync(2, sequence);
5141
+ } catch (err) {}
5142
+ }
5143
+ /**
5144
+ * Writes a progress line to stderr, so stdout keeps carrying only the command report.
5145
+ *
5146
+ * Without a terminal the displays fall back to these discrete milestones, which are just as
5147
+ * much part of the progress display as an in-place redraw and therefore share its stream.
5148
+ */
5149
+ function logProgressLine(message) {
5150
+ if (logger.isEnabledFor("info")) console.error(message);
5151
+ }
5152
+ /** Writes a settle message on the stream matching its severity. */
5153
+ function logSettle(message, level) {
5154
+ if (level === "warn") {
5155
+ logger.warn(message);
5156
+ return;
5157
+ }
5158
+ logProgressLine(message);
5159
+ }
5160
+ /** Restores the cursor and abandons the active progress line; safe to call at any time. */
5161
+ function restoreTerminal() {
5162
+ if (!cursorHidden) return;
5163
+ cursorHidden = false;
5164
+ writeToStderr(`${CLEAR_LINE}${CURSOR_SHOW}`);
5165
+ }
5166
+ /** Restores the cursor, then re-raises the signal with the default disposition. */
5167
+ function handleTerminationSignal(signal) {
5168
+ restoreTerminal();
5169
+ removeTerminalGuards();
5170
+ process.kill(process.pid, signal);
5171
+ }
5172
+ /** Installs exit, crash and signal hooks so a hidden cursor is always restored. */
5173
+ function installTerminalGuards() {
5174
+ if (guardsInstalled) return;
5175
+ guardsInstalled = true;
5176
+ process.on("exit", restoreTerminal);
5177
+ process.on("uncaughtExceptionMonitor", restoreTerminal);
5178
+ process.on("SIGINT", handleTerminationSignal);
5179
+ process.on("SIGTERM", handleTerminationSignal);
5180
+ }
5181
+ /** Removes the cursor guards once no dynamic render is active. */
5182
+ function removeTerminalGuards() {
5183
+ if (!guardsInstalled) return;
5184
+ guardsInstalled = false;
5185
+ process.off("exit", restoreTerminal);
5186
+ process.off("uncaughtExceptionMonitor", restoreTerminal);
5187
+ process.off("SIGINT", handleTerminationSignal);
5188
+ process.off("SIGTERM", handleTerminationSignal);
5189
+ }
5190
+ /** Builds the mutable rendering state shared by bars and spinners. */
5191
+ function createRenderState(options) {
5192
+ const stream = options.stream ?? process.stderr;
5193
+ const now = options.now ?? Date.now;
5194
+ return {
5195
+ stream,
5196
+ interactive: options.interactive ?? isProgressInteractive(process.env, stream.isTTY),
5197
+ ownsProcessStderr: options.stream === void 0,
5198
+ label: options.label,
5199
+ now,
5200
+ throttleMs: Math.max(1, options.throttleMs ?? 100),
5201
+ startedAt: now(),
5202
+ lastFrameAt: Number.NEGATIVE_INFINITY,
5203
+ active: false
5204
+ };
5205
+ }
5206
+ /** Truncates a frame so it never wraps onto a second terminal line. */
5207
+ function fitToWidth(state, line) {
5208
+ const limit = Math.max(1, (state.stream.columns ?? DEFAULT_COLUMNS) - 1);
5209
+ if (line.length <= limit) return line;
5210
+ if (limit <= 3) return TRUNCATION.slice(0, limit);
5211
+ return `${line.slice(0, limit - 3)}${TRUNCATION}`;
5212
+ }
5213
+ /** Hides the cursor and arms the exit guards when a dynamic render starts. */
5214
+ function beginFrame(state) {
5215
+ if (state.active) return;
5216
+ state.active = true;
5217
+ if (!state.ownsProcessStderr) {
5218
+ state.stream.write(CURSOR_HIDE);
5219
+ return;
5220
+ }
5221
+ activeDynamicRenders += 1;
5222
+ if (activeDynamicRenders > 1) return;
5223
+ cursorHidden = true;
5224
+ installTerminalGuards();
5225
+ state.stream.write(CURSOR_HIDE);
5226
+ }
5227
+ /** Clears the dynamic line and reveals the cursor once the render stops. */
5228
+ function endFrame(state) {
5229
+ if (!state.interactive || !state.active) return;
5230
+ state.active = false;
5231
+ state.stream.write(CLEAR_LINE);
5232
+ if (!state.ownsProcessStderr) {
5233
+ state.stream.write(CURSOR_SHOW);
5234
+ return;
5235
+ }
5236
+ activeDynamicRenders = Math.max(0, activeDynamicRenders - 1);
5237
+ if (activeDynamicRenders === 0) {
5238
+ cursorHidden = false;
5239
+ removeTerminalGuards();
5240
+ state.stream.write(CURSOR_SHOW);
5241
+ }
5242
+ }
5243
+ /** Draws one frame, throttled unless `force`d, replacing whatever the line held. */
5244
+ function drawFrame(state, line, force = false) {
5245
+ if (!state.interactive) return;
5246
+ const now = state.now();
5247
+ if (!force && now - state.lastFrameAt < state.throttleMs) return;
5248
+ state.lastFrameAt = now;
5249
+ beginFrame(state);
5250
+ state.stream.write(`${CLEAR_LINE}${fitToWidth(state, line)}${FRAME_TAIL}`);
5251
+ }
5252
+ /** Formats a duration in milliseconds as seconds. */
5253
+ function formatDuration(ms) {
5254
+ const seconds = Math.max(0, ms) / 1e3;
5255
+ return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
5256
+ }
5257
+ /** Formats a completed transfer as size, duration and average speed. */
5258
+ function formatTransferSummary(bytes, elapsedMs) {
5259
+ const elapsed = Math.max(elapsedMs, 1);
5260
+ return `${formatBytes(bytes)} in ${formatDuration(elapsed)} (${formatBytes(bytes / (elapsed / 1e3))}/s)`;
5261
+ }
5262
+ /** Builds the `[====> ]` fill for a completion ratio. */
5263
+ function buildBar(ratio, width) {
5264
+ const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
5265
+ if (filled >= width) return "=".repeat(width);
5266
+ return `${"=".repeat(filled)}>${" ".repeat(width - filled - 1)}`;
5267
+ }
5268
+ /** Picks a bar width that keeps the whole frame inside the terminal. */
5269
+ function barWidthFor(state, reserved) {
5270
+ const columns = state.stream.columns ?? DEFAULT_COLUMNS;
5271
+ return Math.max(MIN_BAR_WIDTH, Math.min(MAX_BAR_WIDTH, columns - reserved));
5272
+ }
5273
+ /** Renders one deterministic transfer frame. */
5274
+ function renderBarFrame(state, transferred, total) {
5275
+ const elapsedMs = state.now() - state.startedAt;
5276
+ const speed = transferred / (Math.max(elapsedMs, 1) / 1e3);
5277
+ const speedText = `${formatBytes(speed)}/s`;
5278
+ if (!total || total <= 0) return `${state.label} ${formatBytes(transferred)} (${speedText}, ${formatDuration(elapsedMs)})`;
5279
+ const ratio = Math.max(0, Math.min(1, transferred / total));
5280
+ const percent = Math.floor(ratio * 100);
5281
+ const eta = speed > 0 && transferred < total ? `, ETA ${formatDuration((total - transferred) / speed * 1e3)}` : "";
5282
+ const tail = ` ${percent}% (${formatBytes(transferred)} / ${formatBytes(total)}, ${speedText}${eta})`;
5283
+ const width = barWidthFor(state, state.label.length + tail.length + 2 + 4);
5284
+ return `${state.label} [${buildBar(ratio, width)}]${tail}`;
5285
+ }
5286
+ /** Renders one indeterminate spinner frame. */
5287
+ function renderSpinnerFrame(state, frame) {
5288
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? "|";
5289
+ return `${state.label} ${glyph} (${formatDuration(state.now() - state.startedAt)})`;
5290
+ }
5291
+ /** Creates a byte progress bar; without a terminal it logs start, milestones and completion. */
5292
+ function createProgressBar(options) {
5293
+ const state = createRenderState(options);
5294
+ let transferred = 0;
5295
+ let total = options.total !== void 0 && options.total > 0 ? options.total : void 0;
5296
+ let milestone = 0;
5297
+ if (!state.interactive && options.announce !== false) logProgressLine(`${state.label}${total ? ` (${formatBytes(total)})` : ""}...`);
5298
+ if (state.interactive) drawFrame(state, renderBarFrame(state, 0, total), true);
5299
+ const settle = (message, level) => {
5300
+ endFrame(state);
5301
+ if (message) logSettle(message, level);
5302
+ };
5303
+ return {
5304
+ update: (next, nextTotal) => {
5305
+ transferred = Math.max(transferred, next);
5306
+ if (nextTotal !== void 0 && nextTotal > 0) total = nextTotal;
5307
+ if (total !== void 0 && transferred > total) total = void 0;
5308
+ if (state.interactive) {
5309
+ drawFrame(state, renderBarFrame(state, transferred, total));
5310
+ return;
5311
+ }
5312
+ if (!total) return;
5313
+ const ratio = transferred / total;
5314
+ while (milestone < PROGRESS_MILESTONES.length && ratio >= (PROGRESS_MILESTONES[milestone] ?? 1)) {
5315
+ const reached = PROGRESS_MILESTONES[milestone] ?? 1;
5316
+ milestone += 1;
5317
+ logProgressLine(`${state.label}: ${Math.round(reached * 100)}% (${formatBytes(transferred)} / ${formatBytes(total)})`);
5318
+ }
5319
+ },
5320
+ finish: (message) => settle(message, "info"),
5321
+ fail: (message) => settle(message, "warn")
5322
+ };
5323
+ }
5324
+ /** Creates an indeterminate spinner driven by a timer and by manual ticks from sync loops. */
5325
+ function createSpinner(options) {
5326
+ const state = createRenderState(options);
5327
+ let frame = 0;
5328
+ let timer;
5329
+ const clearTimer = () => {
5330
+ if (timer !== void 0) {
5331
+ clearInterval(timer);
5332
+ timer = void 0;
5333
+ }
5334
+ };
5335
+ const settle = (message, level) => {
5336
+ clearTimer();
5337
+ endFrame(state);
5338
+ if (message) logSettle(message, level);
5339
+ };
5340
+ return {
5341
+ start: () => {
5342
+ if (!state.interactive) {
5343
+ if (options.announce !== false) logProgressLine(`${state.label}...`);
5344
+ return;
5345
+ }
5346
+ drawFrame(state, renderSpinnerFrame(state, frame), true);
5347
+ if (timer === void 0) {
5348
+ timer = setInterval(() => {
5349
+ frame += 1;
5350
+ drawFrame(state, renderSpinnerFrame(state, frame), true);
5351
+ }, state.throttleMs);
5352
+ timer.unref();
5353
+ }
5354
+ },
5355
+ tick: () => {
5356
+ frame += 1;
5357
+ drawFrame(state, renderSpinnerFrame(state, frame));
5358
+ },
5359
+ updateText: (label) => {
5360
+ state.label = label;
5361
+ drawFrame(state, renderSpinnerFrame(state, frame), true);
5362
+ },
5363
+ stop: (message) => settle(message, "info"),
5364
+ fail: (message) => settle(message, "warn")
5365
+ };
5366
+ }
5367
+ //#endregion
5049
5368
  //#region src/annotations-realms.ts
5050
5369
  /** Side markers emitted by the upstream docgen, mapped to the realm that owns them. */
5051
5370
  const SIDE_MARKERS = [
@@ -5055,6 +5374,8 @@ const SIDE_MARKERS = [
5055
5374
  ["authority-only", "shared"],
5056
5375
  ["network-authority", "shared"]
5057
5376
  ];
5377
+ /** Number of source lines parsed between two spinner ticks while splitting realms. */
5378
+ const PARSE_PROGRESS_STEP_LINES = 2e3;
5058
5379
  /** Finds the realm declared by the side marker of a documentation block. */
5059
5380
  function realmOfDocBlock(doc) {
5060
5381
  for (const line of doc) for (const [image, realm] of SIDE_MARKERS) if (line.includes(`assets/${image}.png`)) return realm;
@@ -5085,10 +5406,17 @@ function kindOf(declaration) {
5085
5406
  return "other";
5086
5407
  }
5087
5408
  /** Segments annotations into comment runs plus their optional declaration statement. */
5088
- function parseAnnotationUnits(text) {
5409
+ function parseAnnotationUnits(text, onProgress) {
5089
5410
  const lines = text.split(/\r?\n/);
5090
5411
  const units = [];
5091
5412
  let index = 0;
5413
+ let nextTick = PARSE_PROGRESS_STEP_LINES;
5414
+ const tick = () => {
5415
+ if (onProgress !== void 0 && index >= nextTick) {
5416
+ nextTick = index + PARSE_PROGRESS_STEP_LINES;
5417
+ onProgress();
5418
+ }
5419
+ };
5092
5420
  const push = (doc, decl) => {
5093
5421
  const first = decl?.[0] ?? null;
5094
5422
  units.push({
@@ -5102,6 +5430,7 @@ function parseAnnotationUnits(text) {
5102
5430
  });
5103
5431
  };
5104
5432
  while (index < lines.length) {
5433
+ tick();
5105
5434
  const doc = [];
5106
5435
  while (index < lines.length && (lines[index].startsWith("---") || lines[index].trim() === "")) {
5107
5436
  if (lines[index].startsWith("---")) doc.push(lines[index]);
@@ -5200,11 +5529,12 @@ function renderRealm(units, realm) {
5200
5529
  * and a realm-specific class table is dropped from the other realm unless it still has
5201
5530
  * instance members there.
5202
5531
  */
5203
- function splitAnnotationsByRealm(text) {
5204
- const units = parseAnnotationUnits(text);
5532
+ function splitAnnotationsByRealm(text, onProgress) {
5533
+ const units = parseAnnotationUnits(text, onProgress);
5205
5534
  const classRealms = classRealmsOf(units);
5206
5535
  const crossRealm = crossRealmClasses(units);
5207
5536
  const memberRealms = markedMemberRealms(units);
5537
+ onProgress?.();
5208
5538
  for (const unit of units) {
5209
5539
  if (unit.owner) {
5210
5540
  unit.realm = unit.owner;
@@ -5218,8 +5548,11 @@ function splitAnnotationsByRealm(text) {
5218
5548
  const classRealm = unit.receiver ? classRealms.get(unit.receiver) : void 0;
5219
5549
  unit.realm = classRealm && classRealm !== "shared" && !crossRealm.has(unit.receiver) ? classRealm : "shared";
5220
5550
  }
5551
+ onProgress?.();
5552
+ const client = renderRealm(units, "client");
5553
+ onProgress?.();
5221
5554
  return {
5222
- client: renderRealm(units, "client"),
5555
+ client,
5223
5556
  server: renderRealm(units, "server")
5224
5557
  };
5225
5558
  }
@@ -5249,17 +5582,204 @@ function deriveRealmAnnotationFiles(annotationsPath, cacheDir) {
5249
5582
  client: clientPath,
5250
5583
  server: serverPath
5251
5584
  };
5252
- const split = splitAnnotationsByRealm(fs.readFileSync(annotationsPath, "utf-8"));
5253
- fs.mkdirSync(targetDir, { recursive: true });
5254
- writeAtomicFileSync(clientPath, split.client);
5255
- writeAtomicFileSync(serverPath, split.server);
5256
- logger.debug(`[realms] Derived realm annotations in ${targetDir}.`);
5585
+ const spinner = createSpinner({ label: "[realms] Deriving client/server annotations" });
5586
+ spinner.start();
5587
+ try {
5588
+ const split = splitAnnotationsByRealm(fs.readFileSync(annotationsPath, "utf-8"), () => spinner.tick());
5589
+ spinner.updateText("[realms] Writing derived annotations");
5590
+ fs.mkdirSync(targetDir, { recursive: true });
5591
+ writeAtomicFileSync(clientPath, split.client);
5592
+ writeAtomicFileSync(serverPath, split.server);
5593
+ logger.debug(`[realms] Derived realm annotations in ${targetDir}.`);
5594
+ spinner.stop(`[realms] Derived client/server annotations in ${targetDir}.`);
5595
+ } catch (err) {
5596
+ spinner.fail();
5597
+ throw err;
5598
+ }
5257
5599
  return {
5258
5600
  client: clientPath,
5259
5601
  server: serverPath
5260
5602
  };
5261
5603
  }
5262
5604
  //#endregion
5605
+ //#region src/download-guard.ts
5606
+ /**
5607
+ * Shared transport policy for every outbound download performed by nanos-lint.
5608
+ *
5609
+ * `SECURITY.md` requires download requests *and* HTTP redirects to use HTTPS and to stay on
5610
+ * GitHub infrastructure. Redirects are therefore resolved hop by hop in `"manual"` mode: the
5611
+ * default `"follow"` mode contacts the redirect target before any check can run, which would
5612
+ * let a redirect downgrade to plaintext `http://` or reach an internal address.
5613
+ */
5614
+ const ALLOWED_DOWNLOAD_DOMAINS = ["github.com", "githubusercontent.com"];
5615
+ /** Headers that must never follow a redirect to another origin. */
5616
+ const CREDENTIAL_HEADERS = [
5617
+ "authorization",
5618
+ "cookie",
5619
+ "proxy-authorization"
5620
+ ];
5621
+ /** Returns the origin of an already validated URL, or null when it cannot be parsed. */
5622
+ function originOf(urlString) {
5623
+ try {
5624
+ return new URL(urlString).origin;
5625
+ } catch {
5626
+ return null;
5627
+ }
5628
+ }
5629
+ /**
5630
+ * Returns the caller's headers with credentials removed.
5631
+ *
5632
+ * The runtime strips `Authorization` when it follows a redirect by itself, so dropping it here
5633
+ * keeps that guarantee once the guard re-issues each hop manually instead of following it.
5634
+ */
5635
+ function withoutCredentialHeaders(headers) {
5636
+ if (headers === void 0) return;
5637
+ const stripped = new Headers(headers);
5638
+ for (const name of CREDENTIAL_HEADERS) stripped.delete(name);
5639
+ return stripped;
5640
+ }
5641
+ /** Validates that a URL uses HTTPS and targets an allowlisted GitHub host. */
5642
+ function isAllowedDownloadUrl(urlString) {
5643
+ try {
5644
+ const parsed = new URL(urlString);
5645
+ if (parsed.protocol !== "https:") return false;
5646
+ const hostname = parsed.hostname.toLowerCase();
5647
+ return ALLOWED_DOWNLOAD_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
5648
+ } catch {
5649
+ return false;
5650
+ }
5651
+ }
5652
+ /**
5653
+ * Releases a response body without letting cleanup failures mask the block reason.
5654
+ *
5655
+ * Every failure mode is swallowed on purpose: a missing body, a double that exposes no
5656
+ * `cancel()`, and a `cancel()` that rejects or returns a non-promise must all leave the
5657
+ * caller free to report why the response was refused.
5658
+ */
5659
+ async function cancelResponseBody(res) {
5660
+ try {
5661
+ if (typeof res.body?.cancel === "function") await res.body.cancel();
5662
+ } catch (err) {}
5663
+ }
5664
+ /** Reads the `Location` header of a redirect response, tolerating minimal test doubles. */
5665
+ function getRedirectLocation(res) {
5666
+ if (typeof res.headers?.get !== "function") return null;
5667
+ return res.headers.get("location");
5668
+ }
5669
+ /**
5670
+ * Reads the `content-length` of a response whose bytes are streamed as received.
5671
+ *
5672
+ * A content-encoded response (`content-encoding: gzip`, `br`, ...) is decoded by the runtime
5673
+ * while its `content-length` still counts the compressed payload, so that value describes
5674
+ * neither the byte stream a progress bar counts nor the text that reaches the cache. Such a
5675
+ * response reports no usable length and progress falls back to transferred bytes and speed.
5676
+ */
5677
+ function parseDeclaredContentLength(res) {
5678
+ const encoding = (res.headers?.get?.("content-encoding") ?? "").trim().toLowerCase();
5679
+ if (encoding !== "" && encoding !== "identity") return;
5680
+ const header = res.headers?.get?.("content-length");
5681
+ if (!header) return;
5682
+ const declared = parseInt(header, 10);
5683
+ return Number.isFinite(declared) && declared > 0 ? declared : void 0;
5684
+ }
5685
+ /** Classifies a response the runtime followed by itself despite `redirect: "manual"`. */
5686
+ function isOpaqueRedirect(res) {
5687
+ return res.type === "opaqueredirect";
5688
+ }
5689
+ /**
5690
+ * Performs a fetch with hop-by-hop redirect validation against the allowlist.
5691
+ *
5692
+ * Each hop must use HTTPS and stay on an allowlisted GitHub host; a hop that does not is never
5693
+ * contacted, because `redirect: "manual"` surfaces the 3xx response instead of following it.
5694
+ * The final response URL is validated as well, which keeps the policy enforced even on a
5695
+ * runtime that ignores `"manual"` and follows redirects on its own.
5696
+ */
5697
+ async function guardedFetch(url, init = {}) {
5698
+ if (!isAllowedDownloadUrl(url)) return {
5699
+ ok: false,
5700
+ reason: "disallowed-url",
5701
+ url
5702
+ };
5703
+ let currentUrl = url;
5704
+ let currentInit = init;
5705
+ let followed = 0;
5706
+ const requestOrigin = originOf(url);
5707
+ for (;;) {
5708
+ let response;
5709
+ try {
5710
+ response = await fetch(currentUrl, {
5711
+ ...currentInit,
5712
+ redirect: "manual"
5713
+ });
5714
+ } catch (err) {
5715
+ return {
5716
+ ok: false,
5717
+ reason: "network-error",
5718
+ url: currentUrl,
5719
+ error: err
5720
+ };
5721
+ }
5722
+ if (isOpaqueRedirect(response)) {
5723
+ await cancelResponseBody(response);
5724
+ return {
5725
+ ok: false,
5726
+ reason: "disallowed-redirect",
5727
+ url: currentUrl
5728
+ };
5729
+ }
5730
+ if (!(response.status >= 300 && response.status < 400)) {
5731
+ if (response.url && !isAllowedDownloadUrl(response.url)) {
5732
+ await cancelResponseBody(response);
5733
+ return {
5734
+ ok: false,
5735
+ reason: "disallowed-redirect",
5736
+ url: response.url
5737
+ };
5738
+ }
5739
+ return {
5740
+ ok: true,
5741
+ response
5742
+ };
5743
+ }
5744
+ const location = getRedirectLocation(response);
5745
+ if (location === null || location.trim() === "") {
5746
+ await cancelResponseBody(response);
5747
+ return {
5748
+ ok: false,
5749
+ reason: "missing-location",
5750
+ url: currentUrl
5751
+ };
5752
+ }
5753
+ let nextUrl;
5754
+ try {
5755
+ nextUrl = new URL(location, currentUrl).href;
5756
+ } catch (err) {
5757
+ nextUrl = "";
5758
+ }
5759
+ if (!isAllowedDownloadUrl(nextUrl)) {
5760
+ await cancelResponseBody(response);
5761
+ return {
5762
+ ok: false,
5763
+ reason: "disallowed-redirect",
5764
+ url: nextUrl || location
5765
+ };
5766
+ }
5767
+ await cancelResponseBody(response);
5768
+ followed++;
5769
+ if (followed > 5) return {
5770
+ ok: false,
5771
+ reason: "too-many-redirects",
5772
+ url: nextUrl
5773
+ };
5774
+ const nextOrigin = originOf(nextUrl);
5775
+ if (requestOrigin !== null && nextOrigin !== null && nextOrigin !== requestOrigin) currentInit = {
5776
+ ...init,
5777
+ headers: withoutCredentialHeaders(init.headers)
5778
+ };
5779
+ currentUrl = nextUrl;
5780
+ }
5781
+ }
5782
+ //#endregion
5263
5783
  //#region src/luals/version.ts
5264
5784
  const FALLBACK_LUALS_VERSION = "3.19.1";
5265
5785
  const DEFAULT_LUALS_VERSION = "latest";
@@ -5291,10 +5811,15 @@ async function fetchLatestLuaLSVersionFromGitHub() {
5291
5811
  try {
5292
5812
  const headers = { "User-Agent": "nanos-lint" };
5293
5813
  if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
5294
- const res = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest", {
5814
+ const result = await guardedFetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest", {
5295
5815
  headers,
5296
5816
  signal: AbortSignal.timeout(5e3)
5297
5817
  });
5818
+ if (!result.ok || !result.response) {
5819
+ if (result.reason && result.reason !== "network-error") logger.warn(`[luals] Refused LuaLS release lookup: requests must stay on allowlisted GitHub infrastructure over HTTPS (${result.url ?? "unknown URL"}).`);
5820
+ return null;
5821
+ }
5822
+ const res = result.response;
5298
5823
  if (res.ok) {
5299
5824
  const data = await res.json();
5300
5825
  const version = typeof data.tag_name === "string" ? sanitizeLuaLSVersion(data.tag_name) : null;
@@ -6090,20 +6615,6 @@ function findExistingLuaLSDir(version, baseCacheDir = getBaseLuaLSCacheDir()) {
6090
6615
  //#region src/luals/download.ts
6091
6616
  const DOWNLOAD_TIMEOUT_MS = 12e4;
6092
6617
  const MAX_ARCHIVE_SIZE_BYTES = 157286400;
6093
- const ALLOWED_DOWNLOAD_DOMAINS = ["github.com", "githubusercontent.com"];
6094
- /**
6095
- * Validates that a download URL uses HTTPS and targets an allowlisted host.
6096
- */
6097
- function isAllowedDownloadUrl(urlString) {
6098
- try {
6099
- const parsed = new URL(urlString);
6100
- if (parsed.protocol !== "https:") return false;
6101
- const hostname = parsed.hostname.toLowerCase();
6102
- return ALLOWED_DOWNLOAD_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
6103
- } catch {
6104
- return false;
6105
- }
6106
- }
6107
6618
  /** Chunk size used when hashing an archive, so a 150 MB asset is never buffered whole. */
6108
6619
  const HASH_CHUNK_SIZE_BYTES = 1048576;
6109
6620
  /**
@@ -6138,6 +6649,15 @@ async function* limitDownloadStream(source) {
6138
6649
  yield chunk;
6139
6650
  }
6140
6651
  }
6652
+ /** Reports cumulative transferred bytes while passing a download stream through unchanged. */
6653
+ async function* trackDownloadProgress(source, onProgress) {
6654
+ let transferred = 0;
6655
+ for await (const chunk of source) {
6656
+ transferred += chunk.length;
6657
+ onProgress(transferred);
6658
+ yield chunk;
6659
+ }
6660
+ }
6141
6661
  /** Returns whether a cache directory holds a complete, executable LuaLS installation. */
6142
6662
  function isCompleteLuaLSInstall(binaryPath, completeMarker, version) {
6143
6663
  if (!fs.existsSync(binaryPath) || !fs.existsSync(completeMarker)) return false;
@@ -6198,28 +6718,27 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
6198
6718
  logger.info(`[luals] Reusing existing LuaLS ${resolvedVersion} installation from ${existingSourceDir}...`);
6199
6719
  fs.cpSync(existingSourceDir, tempDir, { recursive: true });
6200
6720
  } else {
6201
- if (!isAllowedDownloadUrl(url)) throw new LuaLSError(`Refusing to download LuaLS from untrusted URL: ${url}`, "ERR_LUALS_DOWNLOAD", "Download URLs must use HTTPS and target an allowlisted GitHub host.");
6202
6721
  logger.info(`[luals] Downloading LuaLS ${resolvedVersion} from ${url}...`);
6203
6722
  let response = null;
6204
6723
  let lastErr = null;
6205
6724
  for (let attempt = 1; attempt <= 3; attempt++) {
6206
6725
  try {
6207
- const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
6208
- if (res.url && !isAllowedDownloadUrl(res.url)) {
6209
- if (typeof res.body?.cancel === "function") try {
6210
- await res.body.cancel();
6211
- } catch (cancelErr) {}
6212
- logger.warn(`[luals] Redirect to untrusted URL blocked: ${res.url}`);
6213
- throw new LuaLSError(`Redirect to untrusted URL blocked: ${res.url}`, "ERR_LUALS_DOWNLOAD", "Download redirects must stay on allowlisted HTTPS GitHub hosts.");
6214
- }
6215
- if (res.ok && res.body) {
6216
- response = res;
6726
+ const outcome = await guardedFetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
6727
+ if (!outcome.ok || !outcome.response) {
6728
+ if (outcome.reason !== "network-error") {
6729
+ const blockedMessage = `Redirect to untrusted URL blocked: ${outcome.url ?? url}`;
6730
+ logger.warn(`[luals] ${blockedMessage}`);
6731
+ throw new LuaLSError(blockedMessage, "ERR_LUALS_DOWNLOAD", "Download redirects must stay on allowlisted HTTPS GitHub hosts.");
6732
+ }
6733
+ lastErr = outcome.error ?? /* @__PURE__ */ new Error(`Failed to download ${url}`);
6734
+ } else if (outcome.response.ok && outcome.response.body) {
6735
+ response = outcome.response;
6217
6736
  break;
6737
+ } else {
6738
+ const res = outcome.response;
6739
+ await cancelResponseBody(res);
6740
+ lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
6218
6741
  }
6219
- if (typeof res.body?.cancel === "function") try {
6220
- await res.body.cancel();
6221
- } catch (cancelErr) {}
6222
- lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
6223
6742
  } catch (err) {
6224
6743
  if (err instanceof LuaLSError) throw err;
6225
6744
  lastErr = err;
@@ -6235,50 +6754,76 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
6235
6754
  throw new LuaLSError(`Archive size (${contentLength} bytes) exceeds maximum limit (${MAX_ARCHIVE_SIZE_BYTES} bytes)`, "ERR_LUALS_DOWNLOAD", "Verify the LuaLS release asset size or specify a local binary with LUALS_BIN.");
6236
6755
  }
6237
6756
  }
6757
+ const declaredTotal = parseDeclaredContentLength(response);
6758
+ const downloadStartedAt = Date.now();
6759
+ let transferred = 0;
6760
+ const progress = createProgressBar({
6761
+ label: `[luals] Downloading ${info.assetName}`,
6762
+ total: declaredTotal,
6763
+ announce: false
6764
+ });
6238
6765
  const fileStream = fs.createWriteStream(archivePath);
6239
6766
  try {
6240
6767
  const streamSource = typeof Readable.fromWeb === "function" && !("pipe" in response.body) ? Readable.fromWeb(response.body) : response.body;
6241
- await pipeline(streamSource, limitDownloadStream, fileStream);
6768
+ await pipeline(streamSource, limitDownloadStream, (source) => trackDownloadProgress(source, (bytes) => {
6769
+ transferred = bytes;
6770
+ progress.update(bytes, declaredTotal);
6771
+ }), fileStream);
6242
6772
  } catch (streamErr) {
6773
+ progress.fail();
6243
6774
  try {
6244
6775
  if (fs.existsSync(archivePath)) fs.unlinkSync(archivePath);
6245
6776
  } catch (unlinkErr) {}
6246
6777
  if (streamErr instanceof LuaLSError) throw streamErr;
6247
6778
  throw new LuaLSError(`Failed to download LuaLS from ${url}: ${streamErr instanceof Error ? streamErr.message : String(streamErr)}`, "ERR_LUALS_DOWNLOAD", "Check your network connection or specify a custom binary with LUALS_BIN.", { cause: streamErr });
6248
6779
  }
6780
+ progress.finish(`[luals] Downloaded ${info.assetName} (${formatTransferSummary(transferred, Date.now() - downloadStartedAt)})`);
6249
6781
  const archiveSha256 = computeFileSha256(archivePath);
6250
6782
  logger.info(`[luals] Downloaded ${info.assetName} (SHA-256 ${archiveSha256})`);
6251
6783
  logger.info(`[luals] Extracting to ${destDir}...`);
6252
- await validateArchiveMembers(archivePath);
6253
- try {
6254
- const tarArgs = process.platform === "win32" ? [
6255
- "-xf",
6256
- path.basename(archivePath),
6257
- "-C",
6258
- tempDir
6259
- ] : [
6260
- "-xf",
6261
- path.basename(archivePath),
6262
- "--no-same-owner",
6263
- "--no-same-permissions",
6264
- "-C",
6265
- tempDir
6266
- ];
6267
- await execFileAsync$1(getTarBinary(), tarArgs, { cwd: path.dirname(archivePath) });
6268
- } catch (tarErr) {
6269
- if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync$1("powershell.exe", [
6270
- "-NoProfile",
6271
- "-Command",
6272
- `Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
6273
- ]);
6274
- else throw tarErr;
6275
- }
6276
- const extractedSize = getDirectorySize(tempDir);
6277
- if (extractedSize > 524288e3) throw new LuaLSError(`Extracted archive size (${extractedSize} bytes) exceeds maximum limit (${MAX_DECOMPRESSED_SIZE_BYTES} bytes)`, "ERR_LUALS_EXTRACT", "Run 'nanos-lint clean-cache' and ensure there is sufficient disk space.");
6784
+ const spinner = createSpinner({
6785
+ label: `[luals] Extracting ${info.assetName}`,
6786
+ announce: false
6787
+ });
6788
+ spinner.start();
6789
+ let extractedSize;
6278
6790
  try {
6279
- fs.unlinkSync(archivePath);
6280
- } catch (err) {
6281
- logger.debug(`[luals] Failed to delete temporary archive ${archivePath}: ${err instanceof Error ? err.message : String(err)}`);
6791
+ await validateArchiveMembers(archivePath);
6792
+ try {
6793
+ const tarArgs = process.platform === "win32" ? [
6794
+ "-xf",
6795
+ path.basename(archivePath),
6796
+ "-C",
6797
+ tempDir
6798
+ ] : [
6799
+ "-xf",
6800
+ path.basename(archivePath),
6801
+ "--no-same-owner",
6802
+ "--no-same-permissions",
6803
+ "-C",
6804
+ tempDir
6805
+ ];
6806
+ await execFileAsync$1(getTarBinary(), tarArgs, { cwd: path.dirname(archivePath) });
6807
+ } catch (tarErr) {
6808
+ if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync$1("powershell.exe", [
6809
+ "-NoProfile",
6810
+ "-Command",
6811
+ `Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
6812
+ ]);
6813
+ else throw tarErr;
6814
+ }
6815
+ spinner.updateText("[luals] Verifying extracted files");
6816
+ extractedSize = getDirectorySize(tempDir);
6817
+ if (extractedSize > 524288e3) throw new LuaLSError(`Extracted archive size (${extractedSize} bytes) exceeds maximum limit (${MAX_DECOMPRESSED_SIZE_BYTES} bytes)`, "ERR_LUALS_EXTRACT", "Run 'nanos-lint clean-cache' and ensure there is sufficient disk space.");
6818
+ try {
6819
+ fs.unlinkSync(archivePath);
6820
+ } catch (err) {
6821
+ logger.debug(`[luals] Failed to delete temporary archive ${archivePath}: ${err instanceof Error ? err.message : String(err)}`);
6822
+ }
6823
+ spinner.stop(`[luals] Extracted ${formatBytes(extractedSize)} to ${destDir}`);
6824
+ } catch (extractErr) {
6825
+ spinner.fail();
6826
+ throw extractErr;
6282
6827
  }
6283
6828
  }
6284
6829
  const tempBinaryPath = path.join(tempDir, info.binaryRelativePath);
@@ -6340,6 +6885,236 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
6340
6885
  }
6341
6886
  }
6342
6887
  //#endregion
6888
+ //#region src/target-resolver.ts
6889
+ /** Resolves the canonical filesystem path, falling back to original path on failure. */
6890
+ function getCanonicalPath(p) {
6891
+ try {
6892
+ return fs.realpathSync.native ? fs.realpathSync.native(p) : fs.realpathSync(p);
6893
+ } catch {
6894
+ return p;
6895
+ }
6896
+ }
6897
+ /** Determines whether a path points to the root of a filesystem volume. */
6898
+ function isFilesystemRoot(dir) {
6899
+ const stripped = dir.trim();
6900
+ if (stripped === "/" || stripped === "\\" || stripped === "") return true;
6901
+ if (/^[a-zA-Z]:[\\/]?$/.test(stripped)) return true;
6902
+ const norm = path.resolve(stripped);
6903
+ if (norm === "/" || norm === "\\") return true;
6904
+ return /^[a-zA-Z]:[\\/]?$/.test(norm);
6905
+ }
6906
+ /**
6907
+ * Computes the lowest common ancestor directory for a set of paths.
6908
+ * All paths must reside on the same filesystem root or volume.
6909
+ */
6910
+ function findCommonAncestorDirectory(paths) {
6911
+ if (paths.length === 0) return path.resolve(".");
6912
+ const resolved = paths.map((p) => {
6913
+ const abs = path.resolve(p);
6914
+ try {
6915
+ if (fs.existsSync(abs)) return fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
6916
+ } catch (err) {}
6917
+ return path.dirname(abs);
6918
+ });
6919
+ if (resolved.length === 1 && resolved[0]) return resolved[0];
6920
+ const isWin = process.platform === "win32";
6921
+ const splitPaths = resolved.map((p) => path.resolve(p).split(path.sep));
6922
+ const first = splitPaths[0];
6923
+ if (!first || first.length === 0) return path.resolve(".");
6924
+ const firstRoot = first[0];
6925
+ if (!splitPaths.every((p) => {
6926
+ const rootSeg = p[0];
6927
+ if (rootSeg === void 0) return false;
6928
+ return isWin ? rootSeg.toLowerCase() === firstRoot?.toLowerCase() : rootSeg === firstRoot;
6929
+ }) || firstRoot === void 0) throw new ConfigError(`Cannot check paths across different root drives or volumes: ${paths.join(", ")}`, "ERR_MULTIPLE_ROOTS", "Ensure all checked paths are within the same workspace or project root.");
6930
+ const minLen = Math.min(...splitPaths.map((p) => p.length));
6931
+ let commonLen = 0;
6932
+ for (let i = 0; i < minLen; i++) {
6933
+ const segment = first[i];
6934
+ if (segment === void 0) break;
6935
+ if (!splitPaths.every((p) => {
6936
+ const seg = p[i];
6937
+ if (seg === void 0) return false;
6938
+ return isWin ? seg.toLowerCase() === segment.toLowerCase() : seg === segment;
6939
+ })) break;
6940
+ commonLen++;
6941
+ }
6942
+ if (commonLen === 0) throw new ConfigError(`Cannot check paths across different root drives or volumes: ${paths.join(", ")}`, "ERR_MULTIPLE_ROOTS", "Ensure all checked paths are within the same workspace or project root.");
6943
+ let commonPath = first.slice(0, commonLen).join(path.sep);
6944
+ if (!commonPath) commonPath = path.sep;
6945
+ if (isWin && /^[a-zA-Z]:$/.test(commonPath)) commonPath = `${commonPath}\\`;
6946
+ return path.resolve(commonPath);
6947
+ }
6948
+ /**
6949
+ * Searches upward from a starting directory to locate an enclosing project root
6950
+ * containing `.luarc.json`. Returns the start directory if not found.
6951
+ */
6952
+ function findProjectRoot(startDir) {
6953
+ let curr = path.resolve(startDir);
6954
+ while (true) {
6955
+ if (fs.existsSync(path.join(curr, ".luarc.json"))) return curr;
6956
+ if (fs.existsSync(path.join(curr, ".git"))) break;
6957
+ const parent = path.dirname(curr);
6958
+ if (parent === curr) break;
6959
+ curr = parent;
6960
+ }
6961
+ return startDir;
6962
+ }
6963
+ /**
6964
+ * Characters the LuaLS glob matcher treats as syntax.
6965
+ *
6966
+ * LuaLS compiles `files.exclude` with `glob.gitignore` (see its
6967
+ * `script/glob/glob.lua`), whose grammar only understands backslash escapes:
6968
+ * bracket expressions such as `[[]` are parsed as character ranges and match
6969
+ * nothing useful. Escaping is therefore done with `\`.
6970
+ */
6971
+ const GLOB_METACHARACTERS = /* @__PURE__ */ new Set([
6972
+ "*",
6973
+ "?",
6974
+ "[",
6975
+ "]",
6976
+ "{",
6977
+ "}",
6978
+ ",",
6979
+ "\\"
6980
+ ]);
6981
+ /**
6982
+ * Escapes the glob metacharacters of a single path segment so it matches literally.
6983
+ *
6984
+ * Note: `normalizePattern()` in `src/luals/files.ts` rewrites backslashes to path
6985
+ * separators when counting checked files, so these escapes only reach LuaLS. That is
6986
+ * harmless: the counted file set is filtered by the requested target paths anyway.
6987
+ */
6988
+ function escapeGlobSegment(segment) {
6989
+ let escaped = "";
6990
+ for (const char of segment) escaped += GLOB_METACHARACTERS.has(char) ? `\\${char}` : char;
6991
+ return escaped;
6992
+ }
6993
+ /** Escapes glob metacharacters in every segment of a slash-separated relative path. */
6994
+ function escapeGlobPath(relativePath) {
6995
+ return relativePath.split("/").map(escapeGlobSegment).join("/");
6996
+ }
6997
+ /**
6998
+ * Computes glob exclusion patterns for directories and files under workspace root
6999
+ * that are completely outside the requested target paths.
7000
+ */
7001
+ function computeUnrequestedExclusions(root, targetPaths) {
7002
+ const resolvedRoot = path.resolve(root);
7003
+ if (targetPaths.length === 0 || targetPaths.some((t) => t === "." || t === "" || path.resolve(resolvedRoot, t) === resolvedRoot)) return [];
7004
+ const isWin = process.platform === "win32";
7005
+ const canonicalRoot = getCanonicalPath(resolvedRoot);
7006
+ const relTargets = targetPaths.map((t) => {
7007
+ const canonical = getCanonicalPath(path.isAbsolute(t) ? t : path.resolve(canonicalRoot, t));
7008
+ const rel = path.relative(canonicalRoot, canonical).replace(/\\/g, "/");
7009
+ return isWin ? rel.toLowerCase() : rel;
7010
+ }).filter((rel) => !rel.startsWith("../") && rel !== "..");
7011
+ if (relTargets.length === 0 || relTargets.includes("") || relTargets.includes(".")) return [];
7012
+ const exclusions = [];
7013
+ /** Recursively scans directory entries to identify unrequested paths. */
7014
+ function scanDir(dir, relPrefix) {
7015
+ let entries;
7016
+ try {
7017
+ entries = fs.readdirSync(dir, { withFileTypes: true });
7018
+ } catch {
7019
+ return;
7020
+ }
7021
+ for (const entry of entries) {
7022
+ const entryRel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
7023
+ const normEntry = isWin ? entryRel.toLowerCase() : entryRel;
7024
+ let isExactTarget = false;
7025
+ let isAncestorOfTarget = false;
7026
+ let isDescendantOfTarget = false;
7027
+ for (const target of relTargets) {
7028
+ if (normEntry === target) {
7029
+ isExactTarget = true;
7030
+ break;
7031
+ }
7032
+ if (target.startsWith(`${normEntry}/`)) {
7033
+ isAncestorOfTarget = true;
7034
+ break;
7035
+ }
7036
+ if (normEntry.startsWith(`${target}/`)) {
7037
+ isDescendantOfTarget = true;
7038
+ break;
7039
+ }
7040
+ }
7041
+ if (isExactTarget || isDescendantOfTarget) continue;
7042
+ if (isAncestorOfTarget) {
7043
+ if (entry.isDirectory()) scanDir(path.join(dir, entry.name), entryRel);
7044
+ continue;
7045
+ }
7046
+ if (entry.isDirectory()) exclusions.push(`${escapeGlobPath(entryRel)}/**`);
7047
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".lua")) exclusions.push(escapeGlobPath(entryRel));
7048
+ }
7049
+ }
7050
+ scanDir(canonicalRoot, "");
7051
+ return exclusions;
7052
+ }
7053
+ /** Determines whether a workspace-relative file path matches any of the target paths. */
7054
+ function matchesTargetPaths(relativePath, root, targetPaths) {
7055
+ if (targetPaths.length === 0 || targetPaths.length === 1 && (targetPaths[0] === "." || targetPaths[0] === "")) return true;
7056
+ const isWin = process.platform === "win32";
7057
+ const canonicalRoot = getCanonicalPath(root);
7058
+ const normRel = isWin ? relativePath.replace(/\\/g, "/").toLowerCase() : relativePath.replace(/\\/g, "/");
7059
+ for (const rawTarget of targetPaths) {
7060
+ const fromCwd = path.resolve(rawTarget);
7061
+ const canonicalTarget = getCanonicalPath(fs.existsSync(fromCwd) ? fromCwd : path.isAbsolute(rawTarget) ? rawTarget : path.resolve(canonicalRoot, rawTarget));
7062
+ let relTarget = path.relative(canonicalRoot, canonicalTarget).replace(/\\/g, "/");
7063
+ relTarget = stripTrailingSlashes(relTarget).replace(/^\.\//, "");
7064
+ const normTarget = isWin ? relTarget.toLowerCase() : relTarget;
7065
+ if (normTarget === "" || normTarget === ".") return true;
7066
+ if (normRel === normTarget || normRel.startsWith(`${normTarget}/`)) return true;
7067
+ }
7068
+ return false;
7069
+ }
7070
+ /** Keeps only diagnostics for files that match the requested target paths. */
7071
+ function filterReportByTargetPaths(report, root, targetPaths) {
7072
+ if (targetPaths.length === 0 || targetPaths.length === 1 && (targetPaths[0] === "." || targetPaths[0] === "")) return report;
7073
+ const filtered = {};
7074
+ const canonicalRoot = getCanonicalPath(root);
7075
+ for (const [uri, diags] of Object.entries(report)) {
7076
+ const filePath = fileUriToPath(uri);
7077
+ const canonicalFile = getCanonicalPath(path.resolve(filePath));
7078
+ if (matchesTargetPaths(path.relative(canonicalRoot, canonicalFile).replace(/\\/g, "/"), canonicalRoot, targetPaths)) filtered[uri] = diags;
7079
+ }
7080
+ return filtered;
7081
+ }
7082
+ /** Validates and resolves CLI target paths into a common root directory and target list. */
7083
+ function resolveCheckTargets(rawPaths) {
7084
+ const paths = rawPaths && rawPaths.length > 0 ? rawPaths : ["."];
7085
+ const canonicalTargets = paths.map((targetPath) => {
7086
+ const resolved = path.resolve(targetPath);
7087
+ if (!fs.existsSync(resolved)) throw new LuaLSError(`Target path does not exist: ${targetPath}`, "ERR_TARGET_NOT_FOUND", "Verify that the target path exists and is accessible.");
7088
+ return getCanonicalPath(resolved);
7089
+ });
7090
+ const first = paths[0];
7091
+ if (paths.length === 1 && first) {
7092
+ const canonicalFirst = canonicalTargets[0];
7093
+ const startDir = fs.statSync(canonicalFirst).isFile() ? path.dirname(canonicalFirst) : canonicalFirst;
7094
+ const projRoot = findProjectRoot(startDir);
7095
+ if (projRoot !== startDir) {
7096
+ if (isFilesystemRoot(projRoot)) throw new ConfigError(`Cannot check targets across the root filesystem (${projRoot}): checked paths must share a common project directory.`, "ERR_ROOT_ANCESTOR", "Ensure all checked paths reside within a common project directory.");
7097
+ logger.debug(`[targets] Using discovered project root "${projRoot}" for "${first}".`);
7098
+ return {
7099
+ rootPath: projRoot,
7100
+ targetPaths: canonicalTargets
7101
+ };
7102
+ }
7103
+ return {
7104
+ rootPath: first,
7105
+ targetPaths: canonicalTargets
7106
+ };
7107
+ }
7108
+ const ancestor = findCommonAncestorDirectory(canonicalTargets);
7109
+ if (isFilesystemRoot(ancestor)) throw new ConfigError(`Cannot check targets across the root filesystem (${ancestor}): checked paths must share a common project directory.`, "ERR_ROOT_ANCESTOR", "Ensure all checked paths reside within a common project directory.");
7110
+ const rootPath = findProjectRoot(ancestor);
7111
+ if (rootPath !== ancestor) logger.debug(`[targets] Using discovered project root "${rootPath}" for "${ancestor}".`);
7112
+ return {
7113
+ rootPath,
7114
+ targetPaths: canonicalTargets
7115
+ };
7116
+ }
7117
+ //#endregion
6343
7118
  //#region src/luals/files.ts
6344
7119
  /** Directories skipped unless the workspace config overrides `workspace.ignoreDir`. */
6345
7120
  const DEFAULT_IGNORE_DIRS = [
@@ -6463,7 +7238,7 @@ function resolveTargetPath(targetPath) {
6463
7238
  * honoring `workspace.ignoreDir` and `files.exclude` (#27). Absolute paths are returned
6464
7239
  * for single-file targets, and an empty list is returned when the walk fails.
6465
7240
  */
6466
- function listCheckedFiles(targetPath, configPath) {
7241
+ function listCheckedFiles(targetPath, configPath, targetPaths) {
6467
7242
  const absPath = resolveTargetPath(targetPath);
6468
7243
  if (!absPath) return [];
6469
7244
  if (fs.statSync(absPath).isFile()) return absPath.toLowerCase().endsWith(".lua") ? [absPath] : [];
@@ -6478,7 +7253,7 @@ function listCheckedFiles(targetPath, configPath) {
6478
7253
  }
6479
7254
  const ignore = [...toIgnorePatterns(ignoreDirs), ...toIgnorePatterns(excludePatterns)];
6480
7255
  try {
6481
- return ts(LUA_FILE_PATTERN, {
7256
+ const files = ts(LUA_FILE_PATTERN, {
6482
7257
  cwd: absPath,
6483
7258
  ignore,
6484
7259
  dot: true,
@@ -6486,14 +7261,16 @@ function listCheckedFiles(targetPath, configPath) {
6486
7261
  follow: false,
6487
7262
  withFileTypes: true
6488
7263
  }).filter((entry) => entry.isFile()).map((entry) => entry.relativePosix());
7264
+ if (targetPaths && targetPaths.length > 0) return files.filter((f) => matchesTargetPaths(f, absPath, targetPaths));
7265
+ return files;
6489
7266
  } catch (err) {
6490
7267
  logger.warn(`[luals] Failed to walk ${absPath} while listing checked files: ${err instanceof Error ? err.message : String(err)}`);
6491
7268
  return [];
6492
7269
  }
6493
7270
  }
6494
7271
  /** Counts candidate Lua files within targetPath, taking workspace ignoreDir and files.exclude into account (#27). */
6495
- function countCheckedFiles(targetPath, configPath) {
6496
- return listCheckedFiles(targetPath, configPath).length;
7272
+ function countCheckedFiles(targetPath, configPath, targetPaths) {
7273
+ return listCheckedFiles(targetPath, configPath, targetPaths).length;
6497
7274
  }
6498
7275
  //#endregion
6499
7276
  //#region src/luals/runner.ts
@@ -6705,7 +7482,7 @@ async function runLuaLSCheck(targetPath, configPath, options) {
6705
7482
  const normTarget = normalizeComparable(targetFileOnly);
6706
7483
  for (const [rawUri, diags] of Object.entries(diagnostics)) if (normalizeComparable(fileUriToPath(rawUri)) === normTarget) filtered[rawUri] = diags;
6707
7484
  diagnostics = filtered;
6708
- }
7485
+ } else if (options.paths && options.paths.length > 0) diagnostics = filterReportByTargetPaths(diagnostics, checkDir, options.paths);
6709
7486
  let totalProblems = 0;
6710
7487
  let totalErrors = 0;
6711
7488
  let totalWarnings = 0;
@@ -6717,7 +7494,7 @@ async function runLuaLSCheck(targetPath, configPath, options) {
6717
7494
  else if (d.severity === 2) totalWarnings += 1;
6718
7495
  }
6719
7496
  const passed = totalProblems === 0;
6720
- const filesChecked = countCheckedFiles(targetPath, configPath);
7497
+ const filesChecked = countCheckedFiles(targetPath, configPath, options.paths);
6721
7498
  return {
6722
7499
  passed,
6723
7500
  totalProblems,
@@ -6729,6 +7506,179 @@ async function runLuaLSCheck(targetPath, configPath, options) {
6729
7506
  };
6730
7507
  }
6731
7508
  //#endregion
7509
+ //#region src/deps.ts
7510
+ /** Accumulates repeatable --dep arguments. */
7511
+ function collectDeps(val, prev) {
7512
+ const trimmed = val.trim();
7513
+ if (!trimmed) return prev ?? [];
7514
+ return (prev ?? []).concat(trimmed);
7515
+ }
7516
+ /** Normalizes a filesystem path to forward slashes for LuaLS compatibility. */
7517
+ function normalizeSlash(p) {
7518
+ return p.replace(/\\/g, "/");
7519
+ }
7520
+ /**
7521
+ * Resolves package dependencies declared in .luarc.json (nanos.deps) or via CLI (--dep),
7522
+ * resolving transitive dependencies with cycle detection and partitioning them by realm.
7523
+ */
7524
+ function resolvePackageDependencies(root, userConfig, cliDeps) {
7525
+ const visited = /* @__PURE__ */ new Set();
7526
+ const queue = [];
7527
+ if (cliDeps && cliDeps.length > 0) {
7528
+ for (const dep of cliDeps) if (typeof dep === "string" && dep.trim()) queue.push({
7529
+ rawPath: dep.trim(),
7530
+ fromDir: process.cwd()
7531
+ });
7532
+ }
7533
+ const rawConfigDeps = userConfig.nanos?.deps;
7534
+ if (rawConfigDeps !== void 0) {
7535
+ if (Array.isArray(rawConfigDeps)) for (const dep of rawConfigDeps) if (typeof dep === "string" && dep.trim()) queue.push({
7536
+ rawPath: dep.trim(),
7537
+ fromDir: root
7538
+ });
7539
+ else logger.warn(`[deps] Skipping invalid nanos.deps entry: expected a string path.`);
7540
+ else logger.warn(`[deps] Ignoring nanos.deps: expected an array of path strings.`);
7541
+ }
7542
+ const resolvedFiles = [];
7543
+ const resolvedDirs = [];
7544
+ const depConfigMap = /* @__PURE__ */ new Map();
7545
+ const isWin = process.platform === "win32";
7546
+ while (queue.length > 0) {
7547
+ const item = queue.shift();
7548
+ if (!item) continue;
7549
+ const { rawPath, fromDir } = item;
7550
+ const absPath = path.resolve(fromDir, rawPath);
7551
+ if (!fs.existsSync(absPath)) {
7552
+ logger.warn(`[deps] Dependency path not found: "${rawPath}" (skipping).`);
7553
+ continue;
7554
+ }
7555
+ let canonical;
7556
+ try {
7557
+ canonical = fs.realpathSync.native(absPath);
7558
+ } catch (err) {
7559
+ try {
7560
+ canonical = fs.realpathSync(absPath);
7561
+ } catch (innerErr) {
7562
+ canonical = absPath;
7563
+ }
7564
+ }
7565
+ const cycleKey = isWin ? canonical.toLowerCase() : canonical;
7566
+ if (visited.has(cycleKey)) continue;
7567
+ visited.add(cycleKey);
7568
+ let stat;
7569
+ try {
7570
+ stat = fs.statSync(canonical);
7571
+ } catch (err) {
7572
+ continue;
7573
+ }
7574
+ if (stat.isFile()) {
7575
+ if (canonical.toLowerCase().endsWith(".lua")) resolvedFiles.push(normalizeSlash(canonical));
7576
+ else logger.warn(`[deps] Skipping non-Lua dependency file: "${rawPath}".`);
7577
+ continue;
7578
+ }
7579
+ if (stat.isDirectory()) {
7580
+ resolvedDirs.push(canonical);
7581
+ const candidateLuarc = path.join(canonical, ".luarc.json");
7582
+ if (fs.existsSync(candidateLuarc)) try {
7583
+ const depConfig = loadUserConfig(canonical);
7584
+ depConfigMap.set(canonical, depConfig);
7585
+ const transDeps = depConfig.nanos?.deps;
7586
+ if (transDeps !== void 0) {
7587
+ if (Array.isArray(transDeps)) for (const trans of transDeps) if (typeof trans === "string" && trans.trim()) queue.push({
7588
+ rawPath: trans.trim(),
7589
+ fromDir: canonical
7590
+ });
7591
+ else logger.warn(`[deps] Skipping invalid nanos.deps entry in "${canonical}": expected a string path.`);
7592
+ else logger.warn(`[deps] Ignoring nanos.deps in "${canonical}": expected an array of path strings.`);
7593
+ }
7594
+ } catch (err) {
7595
+ logger.warn(`[deps] Failed to parse transitive dependency configuration at "${candidateLuarc}": ${err instanceof Error ? err.message : String(err)}`);
7596
+ }
7597
+ }
7598
+ }
7599
+ const serverSet = /* @__PURE__ */ new Set();
7600
+ const clientSet = /* @__PURE__ */ new Set();
7601
+ const sharedSet = /* @__PURE__ */ new Set();
7602
+ const allSet = /* @__PURE__ */ new Set();
7603
+ for (const file of resolvedFiles) {
7604
+ serverSet.add(file);
7605
+ clientSet.add(file);
7606
+ sharedSet.add(file);
7607
+ allSet.add(file);
7608
+ }
7609
+ for (const depDir of resolvedDirs) {
7610
+ const normDepDir = normalizeSlash(depDir);
7611
+ allSet.add(normDepDir);
7612
+ let depConfig = depConfigMap.get(depDir);
7613
+ if (!depConfig) try {
7614
+ depConfig = loadUserConfig(depDir);
7615
+ } catch (err) {
7616
+ depConfig = {};
7617
+ }
7618
+ const { enabled, mappings } = resolveRealmMappings(depConfig);
7619
+ if (!enabled) {
7620
+ serverSet.add(normDepDir);
7621
+ clientSet.add(normDepDir);
7622
+ sharedSet.add(normDepDir);
7623
+ continue;
7624
+ }
7625
+ const serverFolder = path.join(depDir, "Server");
7626
+ const clientFolder = path.join(depDir, "Client");
7627
+ const sharedFolder = path.join(depDir, "Shared");
7628
+ const hasServer = fs.existsSync(serverFolder);
7629
+ const hasClient = fs.existsSync(clientFolder);
7630
+ const hasShared = fs.existsSync(sharedFolder);
7631
+ if (hasServer || hasClient || hasShared) {
7632
+ if (hasServer) serverSet.add(normalizeSlash(serverFolder));
7633
+ if (hasClient) clientSet.add(normalizeSlash(clientFolder));
7634
+ if (hasShared) {
7635
+ const normShared = normalizeSlash(sharedFolder);
7636
+ serverSet.add(normShared);
7637
+ clientSet.add(normShared);
7638
+ sharedSet.add(normShared);
7639
+ }
7640
+ try {
7641
+ const entries = fs.readdirSync(depDir, { withFileTypes: true });
7642
+ for (const entry of entries) if (entry.isFile() && entry.name.toLowerCase().endsWith(".lua")) {
7643
+ const rootFile = normalizeSlash(path.join(depDir, entry.name));
7644
+ serverSet.add(rootFile);
7645
+ clientSet.add(rootFile);
7646
+ sharedSet.add(rootFile);
7647
+ }
7648
+ } catch (err) {}
7649
+ continue;
7650
+ }
7651
+ if (depConfig.nanos?.realms) {
7652
+ let matchedAnyCustom = false;
7653
+ for (const { pattern, realm } of mappings) {
7654
+ const baseFolder = pattern.replace(/\/\*\*.*$/, "").replace(/\/\*.*$/, "");
7655
+ const targetPath = path.join(depDir, baseFolder);
7656
+ if (fs.existsSync(targetPath)) {
7657
+ matchedAnyCustom = true;
7658
+ const norm = normalizeSlash(targetPath);
7659
+ if (realm === "server") serverSet.add(norm);
7660
+ else if (realm === "client") clientSet.add(norm);
7661
+ else {
7662
+ serverSet.add(norm);
7663
+ clientSet.add(norm);
7664
+ sharedSet.add(norm);
7665
+ }
7666
+ }
7667
+ }
7668
+ if (matchedAnyCustom) continue;
7669
+ }
7670
+ serverSet.add(normDepDir);
7671
+ clientSet.add(normDepDir);
7672
+ sharedSet.add(normDepDir);
7673
+ }
7674
+ return {
7675
+ server: Array.from(serverSet),
7676
+ client: Array.from(clientSet),
7677
+ shared: Array.from(sharedSet),
7678
+ all: Array.from(allSet)
7679
+ };
7680
+ }
7681
+ //#endregion
6732
7682
  //#region src/realms.ts
6733
7683
  /** Normalizes a workspace-relative path for case-insensitive comparison on Windows. */
6734
7684
  function normalizeRelative(candidate) {
@@ -6786,13 +7736,17 @@ function selectedRealms(selection) {
6786
7736
  return ["shared", selection];
6787
7737
  }
6788
7738
  /** Builds a pass config from the merged base config by swapping library and exclusions. */
6789
- function buildPassConfig(baseConfig, realm, annotationsPath, realmAnnotationPath, excludedPatterns) {
7739
+ function buildPassConfig(baseConfig, realm, annotationsPath, realmAnnotationPath, excludedPatterns, dependencyLibraries = []) {
6790
7740
  const config = JSON.parse(JSON.stringify(baseConfig));
6791
7741
  const normalizedFull = annotationsPath.replace(/\\/g, "/");
6792
7742
  const libraries = (config.workspace?.library ?? []).filter((entry) => entry.replace(/\\/g, "/") !== normalizedFull);
6793
7743
  config.workspace = {
6794
7744
  ...config.workspace ?? {},
6795
- library: [realmAnnotationPath, ...libraries]
7745
+ library: Array.from(/* @__PURE__ */ new Set([
7746
+ realmAnnotationPath,
7747
+ ...libraries,
7748
+ ...dependencyLibraries
7749
+ ]))
6796
7750
  };
6797
7751
  if (realm !== "shared" && excludedPatterns.length > 0) config.files = {
6798
7752
  ...config.files ?? {},
@@ -6814,19 +7768,25 @@ function planRealmCheck(options) {
6814
7768
  logger.debug("[realms] Realm mapping disabled; running a single standard pass.");
6815
7769
  return null;
6816
7770
  }
7771
+ const unrequestedExclusions = options.targetPaths ? computeUnrequestedExclusions(resolvedTarget, options.targetPaths) : [];
6817
7772
  const baseConfig = buildWorkspaceConfig(resolvedTarget, options.customConfigPath, {
6818
7773
  ignore: options.ignore,
6819
- annotationsPath
7774
+ annotationsPath,
7775
+ unrequestedExclusions
6820
7776
  });
6821
7777
  const tempConfigs = [];
6822
7778
  const baseConfigPath = writeTempConfig(baseConfig);
6823
7779
  tempConfigs.push(baseConfigPath);
6824
7780
  try {
6825
7781
  const realmFiles = collectRealmFiles(resolvedTarget, mappings, listCheckedFiles(resolvedTarget, baseConfigPath));
7782
+ const isTargetFile = (file) => {
7783
+ if (!options.targetPaths || options.targetPaths.length === 0) return true;
7784
+ return matchesTargetPaths(file, resolvedTarget, options.targetPaths);
7785
+ };
6826
7786
  const realmReportFiles = {
6827
- client: new Set(realmFiles.client.map(normalizeRelative)),
6828
- server: new Set(realmFiles.server.map(normalizeRelative)),
6829
- shared: new Set([...realmFiles.shared, ...realmFiles.unmatched].map(normalizeRelative))
7787
+ client: new Set(realmFiles.client.filter(isTargetFile).map(normalizeRelative)),
7788
+ server: new Set(realmFiles.server.filter(isTargetFile).map(normalizeRelative)),
7789
+ shared: new Set([...realmFiles.shared, ...realmFiles.unmatched].filter(isTargetFile).map(normalizeRelative))
6830
7790
  };
6831
7791
  const hasRealmFolders = realmFiles.client.length > 0 || realmFiles.server.length > 0 || realmFiles.shared.length > 0;
6832
7792
  if (selection === "all" && !hasRealmFolders) {
@@ -6835,14 +7795,22 @@ function planRealmCheck(options) {
6835
7795
  return null;
6836
7796
  }
6837
7797
  const wanted = selectedRealms(selection).filter((realm) => realmReportFiles[realm].size > 0);
7798
+ if (wanted.length === 0) {
7799
+ if (selection !== "all") logger.warn(`[realms] No target files match the requested realm "${selection}"; falling back to a standard check.`);
7800
+ else logger.debug("[realms] No files to report for selected realm pass; running a single standard pass.");
7801
+ for (const tempConfig of tempConfigs) removeTempConfig(tempConfig);
7802
+ return null;
7803
+ }
6838
7804
  const derived = wanted.some((realm) => realm !== "shared") ? deriveRealmAnnotationFiles(annotationsPath) : {
6839
7805
  client: annotationsPath,
6840
7806
  server: annotationsPath
6841
7807
  };
7808
+ const resolvedDeps = resolvePackageDependencies(resolvedTarget, userConfig, options.cliDeps);
6842
7809
  const passes = [];
6843
7810
  for (const realm of wanted) {
6844
7811
  const excluded = mappings.filter((mapping) => mapping.realm !== realm && mapping.realm !== "shared").map((mapping) => mapping.pattern);
6845
- const configPath = writeTempConfig(buildPassConfig(baseConfig, realm, annotationsPath, realm === "client" ? derived.client : realm === "server" ? derived.server : annotationsPath, excluded));
7812
+ const depLibraries = realm === "server" ? resolvedDeps.server : realm === "client" ? resolvedDeps.client : resolvedDeps.shared;
7813
+ const configPath = writeTempConfig(buildPassConfig(baseConfig, realm, annotationsPath, realm === "client" ? derived.client : realm === "server" ? derived.server : annotationsPath, excluded, depLibraries));
6846
7814
  tempConfigs.push(configPath);
6847
7815
  passes.push({
6848
7816
  realm,
@@ -6873,9 +7841,15 @@ function removeTempConfig(configPath) {
6873
7841
  /** Keeps only the diagnostics of files owned by the pass. */
6874
7842
  function filterReportByFiles(report, root, reportFiles) {
6875
7843
  const filtered = {};
7844
+ const canonicalRoot = getCanonicalPath(root);
6876
7845
  for (const [uri, diagnostics] of Object.entries(report)) {
6877
7846
  const filePath = fileUriToPath(uri);
6878
- const relative = normalizeRelative(path.relative(root, path.resolve(filePath)));
7847
+ const resolvedFile = path.resolve(filePath);
7848
+ let relative = normalizeRelative(path.relative(root, resolvedFile));
7849
+ if (!reportFiles.has(relative)) {
7850
+ const canonicalFile = getCanonicalPath(resolvedFile);
7851
+ relative = normalizeRelative(path.relative(canonicalRoot, canonicalFile));
7852
+ }
6879
7853
  if (reportFiles.has(relative)) filtered[uri] = diagnostics;
6880
7854
  }
6881
7855
  return filtered;
@@ -10116,7 +11090,7 @@ const DOCGEN_BRANCH = "docgen-output";
10116
11090
  const RAW_ANNOTATIONS_URL = "https://raw.githubusercontent.com/nanos-world/vscode-extension/refs/heads/docgen-output/annotations.lua";
10117
11091
  const GITHUB_COMMITS_API = "https://api.github.com/repos/nanos-world/vscode-extension/commits/docgen-output";
10118
11092
  /** Reads an HTTP response body with an upper byte limit to guard against memory exhaustion. */
10119
- async function readBoundedResponseBody(res, maxBytes, onExceeded) {
11093
+ async function readBoundedResponseBody(res, maxBytes, onExceeded, onProgress) {
10120
11094
  const lengthHeader = res.headers?.get?.("content-length");
10121
11095
  if (lengthHeader) {
10122
11096
  const declared = parseInt(lengthHeader, 10);
@@ -10137,6 +11111,7 @@ async function readBoundedResponseBody(res, maxBytes, onExceeded) {
10137
11111
  onExceeded(total, "stream");
10138
11112
  return null;
10139
11113
  }
11114
+ onProgress?.(total);
10140
11115
  chunks.push(buf);
10141
11116
  }
10142
11117
  return Buffer.concat(chunks).toString("utf-8");
@@ -10152,15 +11127,35 @@ async function readBoundedResponseBody(res, maxBytes, onExceeded) {
10152
11127
  }
10153
11128
  return "";
10154
11129
  }
11130
+ /**
11131
+ * Resolves a request through the download guard, which refuses an off-allowlist request URL,
11132
+ * an off-allowlist redirect hop and an off-allowlist final response URL, releasing the body of
11133
+ * every refused response instead of downloading it.
11134
+ */
11135
+ async function resolveGuardedResponse(url, init) {
11136
+ const result = await guardedFetch(url, init);
11137
+ if (result.ok && result.response) return { res: result.response };
11138
+ return { block: result };
11139
+ }
11140
+ /** Builds the user-facing error for a request refused by the download transport policy. */
11141
+ function downloadBlockError(result) {
11142
+ if (result.reason === "network-error") return new AnnotationsError(`Failed to download annotations.lua: ${result.error instanceof Error ? result.error.message : String(result.error)}`, "ERR_ANNOTATIONS_DOWNLOAD", "Verify your internet connection and that GitHub raw endpoints are accessible.", { cause: result.error });
11143
+ return new AnnotationsError(`${result.reason === "too-many-redirects" ? "Redirect chain too long" : result.reason === "missing-location" ? "Redirect without a usable target" : "Redirect blocked"}: requests must stay on allowlisted GitHub infrastructure over HTTPS (${result.url ?? "unknown URL"})`, "ERR_ANNOTATIONS_DOWNLOAD", "Verify that no proxy or DNS override redirects GitHub traffic to another host or to plaintext http://.");
11144
+ }
10155
11145
  /** Fetches the latest commit SHA for the annotations branch from the GitHub API. */
10156
11146
  async function fetchLatestCommitId() {
10157
11147
  try {
10158
11148
  const headers = { "User-Agent": "nanos-lint" };
10159
11149
  if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
10160
- const res = await fetch(GITHUB_COMMITS_API, {
11150
+ const outcome = await resolveGuardedResponse(GITHUB_COMMITS_API, {
10161
11151
  headers,
10162
11152
  signal: AbortSignal.timeout(5e3)
10163
11153
  });
11154
+ if ("block" in outcome) {
11155
+ if (outcome.block.reason !== "network-error") logger.warn(`[annotations] ${downloadBlockError(outcome.block).message}`);
11156
+ return null;
11157
+ }
11158
+ const { res } = outcome;
10164
11159
  if (res.ok) {
10165
11160
  const rawJson = await readBoundedResponseBody(res, MAX_COMMIT_JSON_SIZE_BYTES, (bytes) => {
10166
11161
  logger.warn(`GitHub commits response exceeded size limit of ${MAX_COMMIT_JSON_SIZE_BYTES} bytes (${bytes} bytes)`);
@@ -10187,16 +11182,40 @@ function getRawAnnotationsUrl(commitSha) {
10187
11182
  }
10188
11183
  /** Downloads raw annotations.lua content from GitHub raw content endpoints. */
10189
11184
  async function fetchRawAnnotationsContent(commitSha) {
10190
- const url = getRawAnnotationsUrl(commitSha);
10191
- const res = await fetch(url, {
11185
+ const outcome = await resolveGuardedResponse(getRawAnnotationsUrl(commitSha), {
10192
11186
  headers: { "User-Agent": "nanos-lint" },
10193
11187
  signal: AbortSignal.timeout(15e3)
10194
11188
  });
11189
+ if ("block" in outcome) {
11190
+ const blockError = downloadBlockError(outcome.block);
11191
+ logger.warn(`[annotations] ${blockError.message}`);
11192
+ throw blockError;
11193
+ }
11194
+ const { res } = outcome;
10195
11195
  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.");
10196
- const text = await readBoundedResponseBody(res, MAX_ANNOTATIONS_SIZE_BYTES, (bytes, reason) => {
10197
- throw new AnnotationsError(reason === "header" ? `Annotations file size (${bytes} bytes) exceeds maximum limit (${MAX_ANNOTATIONS_SIZE_BYTES} bytes)` : `Annotations file download exceeded maximum allowed size of ${MAX_ANNOTATIONS_SIZE_BYTES} bytes`, "ERR_ANNOTATIONS_TOO_LARGE", "Specify a local annotations file via --annotations <path>.");
11196
+ const declaredTotal = parseDeclaredContentLength(res);
11197
+ const startedAt = Date.now();
11198
+ let transferred = 0;
11199
+ const commitLabel = commitSha ? ` (${commitSha.slice(0, 7)})` : "";
11200
+ const progress = createProgressBar({
11201
+ label: `[annotations] Downloading ${ANNOTATIONS_FILENAME}${commitLabel}`,
11202
+ total: declaredTotal,
11203
+ announce: false
10198
11204
  });
10199
- 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>.");
11205
+ let text;
11206
+ try {
11207
+ text = await readBoundedResponseBody(res, MAX_ANNOTATIONS_SIZE_BYTES, (bytes, reason) => {
11208
+ throw new AnnotationsError(reason === "header" ? `Annotations file size (${bytes} bytes) exceeds maximum limit (${MAX_ANNOTATIONS_SIZE_BYTES} bytes)` : `Annotations file download exceeded maximum allowed size of ${MAX_ANNOTATIONS_SIZE_BYTES} bytes`, "ERR_ANNOTATIONS_TOO_LARGE", "Specify a local annotations file via --annotations <path>.");
11209
+ }, (bytes) => {
11210
+ transferred = bytes;
11211
+ progress.update(bytes, declaredTotal);
11212
+ });
11213
+ 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>.");
11214
+ } catch (err) {
11215
+ progress.fail();
11216
+ throw err;
11217
+ }
11218
+ progress.finish(`[annotations] Downloaded ${ANNOTATIONS_FILENAME} (${formatTransferSummary(transferred, Date.now() - startedAt)})`);
10200
11219
  return text;
10201
11220
  }
10202
11221
  /**
@@ -10443,15 +11462,16 @@ function createProgram(options) {
10443
11462
  "info",
10444
11463
  "debug",
10445
11464
  "silent"
10446
- ]).default(DEFAULT_LOG_LEVEL)).hook("preAction", (thisCommand, actionCommand) => {
11465
+ ]).default(DEFAULT_LOG_LEVEL)).option("--no-progress", "Disable the interactive download, extraction and realm derivation progress display").hook("preAction", (thisCommand, actionCommand) => {
10447
11466
  const target = actionCommand || thisCommand;
10448
11467
  const opts = target.optsWithGlobals ? target.optsWithGlobals() : target.opts();
10449
11468
  if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
10450
- }).exitOverride().configureOutput({
11469
+ if (opts.progress === false) setProgressMode("off");
11470
+ }).exitOverride().configureHelp({ showGlobalOptions: true }).configureOutput({
10451
11471
  writeOut: (str) => console.log(str.trimEnd()),
10452
11472
  writeErr: (str) => logger.error(str.trimEnd())
10453
11473
  });
10454
- 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([
11474
+ program.command("check [paths...]", { isDefault: true }).description("Check workspace files or directories (default command)").addOption(new Option("--checklevel <level>", "Minimum diagnostic level: Error, Warning, Information, Hint").choices([
10455
11475
  "Error",
10456
11476
  "Warning",
10457
11477
  "Information",
@@ -10460,7 +11480,7 @@ function createProgram(options) {
10460
11480
  "pretty",
10461
11481
  "json",
10462
11482
  "github"
10463
- ])).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").addOption(new Option("-l, --log-level <level>", "Logging level: error, warn, info, debug, silent").choices([
11483
+ ])).option("-i, --ignore <pattern>", "Files or directories to ignore (supports glob patterns, repeatable)", collectIgnorePatterns).option("-d, --dep <path>", "Path to package dependency or Lua definition file (repeatable)", collectDeps).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").addOption(new Option("-l, --log-level <level>", "Logging level: error, warn, info, debug, silent").choices([
10464
11484
  "error",
10465
11485
  "warn",
10466
11486
  "info",
@@ -10471,44 +11491,58 @@ function createProgram(options) {
10471
11491
  "client",
10472
11492
  "server",
10473
11493
  "shared"
10474
- ]).default("all")).action(async (targetPath = ".", opts) => {
11494
+ ]).default("all")).action(async (targetPaths = ["."], opts) => {
10475
11495
  if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
11496
+ const { rootPath, targetPaths: canonicalTargets } = resolveCheckTargets(targetPaths && targetPaths.length > 0 ? targetPaths : ["."]);
10476
11497
  const format = opts.github ? "github" : opts.format || (process.env.GITHUB_ACTIONS ? "github" : "pretty");
11498
+ if (format === "json") {
11499
+ setProgressMode("off");
11500
+ logger.setDiagnosticStream("stderr");
11501
+ }
10477
11502
  const checkOptions = {
10478
- path: targetPath,
11503
+ path: rootPath,
11504
+ paths: canonicalTargets,
10479
11505
  configpath: opts.config,
10480
11506
  checklevel: opts.checklevel,
10481
11507
  format,
10482
11508
  lualsVersion: opts.lualsVersion,
10483
11509
  failOnError: opts.fail !== false,
10484
- ignore: opts.ignore
11510
+ ignore: opts.ignore,
11511
+ deps: opts.dep
10485
11512
  };
10486
11513
  if (opts.config) {
10487
11514
  const resolvedConfig = path.resolve(opts.config);
10488
11515
  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.");
10489
11516
  }
10490
11517
  const annotationsPath = await resolveAnnotations({ customPath: opts.annotations });
11518
+ const userConfig = loadUserConfig(rootPath, checkOptions.configpath);
10491
11519
  const realmPlan = planRealmCheck({
10492
- targetPath,
10493
- userConfig: loadUserConfig(targetPath, checkOptions.configpath),
11520
+ targetPath: rootPath,
11521
+ userConfig,
10494
11522
  selection: opts.realm ?? "all",
10495
11523
  annotationsPath,
10496
11524
  customConfigPath: checkOptions.configpath,
10497
- ignore: checkOptions.ignore
11525
+ ignore: checkOptions.ignore,
11526
+ targetPaths: canonicalTargets,
11527
+ cliDeps: checkOptions.deps
10498
11528
  });
10499
11529
  let result;
10500
11530
  if (realmPlan) try {
10501
- result = await runRealmAwareCheck(realmPlan, targetPath, checkOptions);
11531
+ result = await runRealmAwareCheck(realmPlan, rootPath, checkOptions);
10502
11532
  } finally {
10503
11533
  realmPlan.cleanup();
10504
11534
  }
10505
11535
  else {
10506
- const resolved = resolveWorkspaceConfig(targetPath, checkOptions.configpath, {
11536
+ const resolvedDeps = resolvePackageDependencies(rootPath, userConfig, checkOptions.deps);
11537
+ const unrequestedExclusions = computeUnrequestedExclusions(rootPath, canonicalTargets);
11538
+ const resolved = resolveWorkspaceConfig(rootPath, checkOptions.configpath, {
10507
11539
  ignore: checkOptions.ignore,
10508
- annotationsPath
11540
+ annotationsPath,
11541
+ dependencyLibraries: resolvedDeps.all,
11542
+ unrequestedExclusions
10509
11543
  });
10510
11544
  try {
10511
- result = await runLuaLSCheck(targetPath, resolved.configPath, checkOptions);
11545
+ result = await runLuaLSCheck(rootPath, resolved.configPath, checkOptions);
10512
11546
  } finally {
10513
11547
  if (resolved.isTemp && fs.existsSync(resolved.configPath)) try {
10514
11548
  fs.unlinkSync(resolved.configPath);
@@ -10556,6 +11590,7 @@ function createProgram(options) {
10556
11590
  }
10557
11591
  };
10558
11592
  const handleCacheStatus = (opts) => {
11593
+ if (opts?.json) logger.setDiagnosticStream("stderr");
10559
11594
  const report = getCacheStatus();
10560
11595
  if (opts?.json) writeOutput(JSON.stringify(report, null, 2));
10561
11596
  else writeOutput(formatCacheStatusPretty(report));
@@ -10589,6 +11624,7 @@ Examples:
10589
11624
  $ npx nanos-lint check . --checklevel=Error
10590
11625
  $ npx nanos-lint check . --ignore "myfolder/hello-*.lua"
10591
11626
  $ npx nanos-lint check . --realm server
11627
+ $ npx nanos-lint check Shared/ Server/ --realm server
10592
11628
  $ npx nanos-lint init
10593
11629
  $ npx nanos-lint clean-cache
10594
11630
  `);
@@ -10596,6 +11632,8 @@ Examples:
10596
11632
  }
10597
11633
  /** Parses command-line arguments and executes the requested CLI action. */
10598
11634
  async function runCLI(args = process.argv.slice(2)) {
11635
+ setProgressMode("auto");
11636
+ logger.setDiagnosticStream("stdout");
10599
11637
  for (let i = 0; i < args.length; i++) {
10600
11638
  const arg = args[i];
10601
11639
  if (!arg) continue;
@@ -10668,6 +11706,6 @@ if (isDirectExecution()) runCLI().then((code) => {
10668
11706
  process.exit(1);
10669
11707
  });
10670
11708
  //#endregion
10671
- export { getBaseLuaLSCacheDir as $, getDirectorySize as $t, formatProblemSummary as A, VALID_LUALS_DIAGNOSTIC_CODES as At, runLuaLSCheck as B, parseJsonc as Bt, getCachedAnnotationsFilePath as C, fetchLatestLuaLSVersionFromGitHub as Ct, updateLastCheckedDate as D, deriveRealmAnnotationFiles as Dt, readAnnotationsMetadata as E, sanitizeLuaLSVersion as Et, shouldEnableColor as F, getPackageRoot as Ft, MAX_ARCHIVE_SIZE_BYTES as G, writeTempConfig as Gt, listCheckedFiles as H, resolveWorkspaceConfig as Ht, collectRealmFiles as I, initWorkspace as It, isAllowedDownloadUrl as J, ConfigError as Jt, computeFileSha256 as K, AnnotationsError as Kt, planRealmCheck as L, loadConfigFile as Lt, formatSeverityBadge as M, getDefaultAnnotationsPath as Mt, getColors as N, getDefaultTemplatePath as Nt, formatGitHubAnnotations as O, splitAnnotationsByRealm as Ot, pluralize as P, getDefinitionsDir as Pt, findExistingLuaLSDir as Q, formatBytes as Qt, runRealmAwareCheck as R, loadUserConfig as Rt, getAnnotationsMetadataFilePath as S, FALLBACK_LUALS_VERSION as St, isAnnotationsValid as T, resolveLuaLSVersion as Tt, ALLOWED_DOWNLOAD_DOMAINS as U, stripJsonComments as Ut, countCheckedFiles as V, resolveRealmMappings as Vt, DOWNLOAD_TIMEOUT_MS as W, stripTrailingSlashes as Wt, LUALS_METADATA_FILENAME as X, NanosLintError as Xt, limitDownloadStream as Y, LuaLSError as Yt, cleanupOldCachedLuaLSVersions as Z, cleanCache as Zt, MAX_ANNOTATIONS_SIZE_BYTES as _, isZipArchive as _t, formatCacheStatusPretty as a, Logger as an, readLuaLSMetadata as at, MIN_ANNOTATIONS_SIZE_BYTES as b, getPlatformInfo as bt, DOCGEN_BRANCH as c, parseLogLevel as cn, MAX_DECOMPRESSED_SIZE_BYTES as ct, RAW_ANNOTATIONS_URL as d, escapePowerShellSingleQuote as dt, systemPaths as en, getCacheDir as et, downloadAndCacheAnnotations as f, getTarBinary as ft, ANNOTATIONS_FILENAME as g, isGzipArchive as gt, getRawAnnotationsUrl as h, isBinaryValid as ht, runCLI as i, LOG_LEVEL_PRIORITY as in, listCachedLuaLSVersions as it, formatReport as j, buildWorkspaceConfig as jt, formatPretty as k, DEFAULT_REALM_MAPPINGS as kt, DOCGEN_REPO as l, MIN_DOWNLOADED_BINARY_SIZE_BYTES as lt, fetchRawAnnotationsContent as m, isBinaryRunnable as mt, createProgram as n, DEFAULT_LOG_LEVEL as nn, getLuaLSMetadataPath as nt, getCacheStatus as o, isValidLogLevel as on, writeLuaLSMetadata as ot, fetchLatestCommitId as p, inspectZipMembers as pt, downloadAndExtractLuaLS as q, CacheError as qt, isDirectExecution as r, LOG_LEVELS as rn, listCachedLuaLSVersionDirs as rt, resolveAnnotations as s, logger as sn, MAX_ARCHIVE_MEMBER_COUNT as st, collectIgnorePatterns as t, fileUriToPath as tn, getIsoWeek as tt, GITHUB_COMMITS_API as u, assertValidLuaLSBinary as ut, MAX_COMMIT_JSON_SIZE_BYTES as v, parseTarTvSize as vt, getTodayDateString as w, resolveLatestLuaLSVersion as wt, getAnnotationsCacheDir as x, DEFAULT_LUALS_VERSION as xt, METADATA_FILENAME as y, validateArchiveMembers as yt, resolveLuaLSBinary as z, mergeConfigs as zt };
11709
+ export { DOWNLOAD_TIMEOUT_MS as $, getDefaultAnnotationsPath as $t, formatProblemSummary as A, DEFAULT_LUALS_VERSION as At, resolvePackageDependencies as B, PROGRESS_MILESTONES as Bt, getCachedAnnotationsFilePath as C, DEFAULT_LOG_LEVEL as Cn, isBinaryRunnable as Ct, updateLastCheckedDate as D, isValidLogLevel as Dn, parseTarTvSize as Dt, readAnnotationsMetadata as E, Logger as En, isZipArchive as Et, shouldEnableColor as F, sanitizeLuaLSVersion as Ft, computeUnrequestedExclusions as G, formatTransferSummary as Gt, runLuaLSCheck as H, SPINNER_FRAMES as Ht, collectRealmFiles as I, ALLOWED_DOWNLOAD_DOMAINS as It, findProjectRoot as J, restoreTerminal as Jt, filterReportByTargetPaths as K, getProgressMode as Kt, planRealmCheck as L, isAllowedDownloadUrl as Lt, formatSeverityBadge as M, fetchLatestLuaLSVersionFromGitHub as Mt, getColors as N, resolveLatestLuaLSVersion as Nt, formatGitHubAnnotations as O, logger as On, validateArchiveMembers as Ot, pluralize as P, resolveLuaLSVersion as Pt, resolveCheckTargets as Q, buildWorkspaceConfig as Qt, runRealmAwareCheck as R, deriveRealmAnnotationFiles as Rt, getAnnotationsMetadataFilePath as S, fileUriToPath as Sn, inspectZipMembers as St, isAnnotationsValid as T, LOG_LEVEL_PRIORITY as Tn, isGzipArchive as Tt, countCheckedFiles as U, createProgressBar as Ut, resolveLuaLSBinary as V, PROGRESS_THROTTLE_MS as Vt, listCheckedFiles as W, createSpinner as Wt, isFilesystemRoot as X, DEFAULT_REALM_MAPPINGS as Xt, getCanonicalPath as Y, setProgressMode as Yt, matchesTargetPaths as Z, VALID_LUALS_DIAGNOSTIC_CODES as Zt, MAX_ANNOTATIONS_SIZE_BYTES as _, NanosLintError as _n, MAX_DECOMPRESSED_SIZE_BYTES as _t, formatCacheStatusPretty as a, loadUserConfig as an, LUALS_METADATA_FILENAME as at, MIN_ANNOTATIONS_SIZE_BYTES as b, getDirectorySize as bn, escapePowerShellSingleQuote as bt, DOCGEN_BRANCH as c, resolveRealmMappings as cn, getBaseLuaLSCacheDir as ct, RAW_ANNOTATIONS_URL as d, stripTrailingSlashes as dn, getLuaLSMetadataPath as dt, getDefaultTemplatePath as en, MAX_ARCHIVE_SIZE_BYTES as et, downloadAndCacheAnnotations as f, writeTempConfig as fn, listCachedLuaLSVersionDirs as ft, ANNOTATIONS_FILENAME as g, LuaLSError as gn, MAX_ARCHIVE_MEMBER_COUNT as gt, getRawAnnotationsUrl as h, ConfigError as hn, writeLuaLSMetadata as ht, runCLI as i, loadConfigFile as in, trackDownloadProgress as it, formatReport as j, FALLBACK_LUALS_VERSION as jt, formatPretty as k, parseLogLevel as kn, getPlatformInfo as kt, DOCGEN_REPO as l, resolveWorkspaceConfig as ln, getCacheDir as lt, fetchRawAnnotationsContent as m, CacheError as mn, readLuaLSMetadata as mt, createProgram as n, getPackageRoot as nn, downloadAndExtractLuaLS as nt, getCacheStatus as o, mergeConfigs as on, cleanupOldCachedLuaLSVersions as ot, fetchLatestCommitId as p, AnnotationsError as pn, listCachedLuaLSVersions as pt, findCommonAncestorDirectory as q, isProgressInteractive as qt, isDirectExecution as r, initWorkspace as rn, limitDownloadStream as rt, resolveAnnotations as s, parseJsonc as sn, findExistingLuaLSDir as st, collectIgnorePatterns as t, getDefinitionsDir as tn, computeFileSha256 as tt, GITHUB_COMMITS_API as u, stripJsonComments as un, getIsoWeek as ut, MAX_COMMIT_JSON_SIZE_BYTES as v, cleanCache as vn, MIN_DOWNLOADED_BINARY_SIZE_BYTES as vt, getTodayDateString as w, LOG_LEVELS as wn, isBinaryValid as wt, getAnnotationsCacheDir as x, systemPaths as xn, getTarBinary as xt, METADATA_FILENAME as y, formatBytes as yn, assertValidLuaLSBinary as yt, collectDeps as z, splitAnnotationsByRealm as zt };
10672
11710
 
10673
- //# sourceMappingURL=cli-B9EpAClV.js.map
11711
+ //# sourceMappingURL=cli-CeNxOiST.js.map