nanos-lint 3.1.0 → 3.2.1
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 +33 -3
- package/action.yml +1 -1
- package/dist/{cli-Cb3wLBCr.js → cli-I16SeKRK.js} +695 -84
- package/dist/cli-I16SeKRK.js.map +1 -0
- package/dist/cli-dxsaVVVq.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +90 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/dist/cli-Cb3wLBCr.js.map +0 -1
|
@@ -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"))
|
|
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"))
|
|
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();
|
|
@@ -5056,6 +5077,294 @@ async function writeAtomicFile(targetPath, data, options = {}) {
|
|
|
5056
5077
|
}
|
|
5057
5078
|
}
|
|
5058
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
|
|
5059
5368
|
//#region src/annotations-realms.ts
|
|
5060
5369
|
/** Side markers emitted by the upstream docgen, mapped to the realm that owns them. */
|
|
5061
5370
|
const SIDE_MARKERS = [
|
|
@@ -5065,6 +5374,8 @@ const SIDE_MARKERS = [
|
|
|
5065
5374
|
["authority-only", "shared"],
|
|
5066
5375
|
["network-authority", "shared"]
|
|
5067
5376
|
];
|
|
5377
|
+
/** Number of source lines parsed between two spinner ticks while splitting realms. */
|
|
5378
|
+
const PARSE_PROGRESS_STEP_LINES = 2e3;
|
|
5068
5379
|
/** Finds the realm declared by the side marker of a documentation block. */
|
|
5069
5380
|
function realmOfDocBlock(doc) {
|
|
5070
5381
|
for (const line of doc) for (const [image, realm] of SIDE_MARKERS) if (line.includes(`assets/${image}.png`)) return realm;
|
|
@@ -5095,10 +5406,17 @@ function kindOf(declaration) {
|
|
|
5095
5406
|
return "other";
|
|
5096
5407
|
}
|
|
5097
5408
|
/** Segments annotations into comment runs plus their optional declaration statement. */
|
|
5098
|
-
function parseAnnotationUnits(text) {
|
|
5409
|
+
function parseAnnotationUnits(text, onProgress) {
|
|
5099
5410
|
const lines = text.split(/\r?\n/);
|
|
5100
5411
|
const units = [];
|
|
5101
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
|
+
};
|
|
5102
5420
|
const push = (doc, decl) => {
|
|
5103
5421
|
const first = decl?.[0] ?? null;
|
|
5104
5422
|
units.push({
|
|
@@ -5112,6 +5430,7 @@ function parseAnnotationUnits(text) {
|
|
|
5112
5430
|
});
|
|
5113
5431
|
};
|
|
5114
5432
|
while (index < lines.length) {
|
|
5433
|
+
tick();
|
|
5115
5434
|
const doc = [];
|
|
5116
5435
|
while (index < lines.length && (lines[index].startsWith("---") || lines[index].trim() === "")) {
|
|
5117
5436
|
if (lines[index].startsWith("---")) doc.push(lines[index]);
|
|
@@ -5210,11 +5529,12 @@ function renderRealm(units, realm) {
|
|
|
5210
5529
|
* and a realm-specific class table is dropped from the other realm unless it still has
|
|
5211
5530
|
* instance members there.
|
|
5212
5531
|
*/
|
|
5213
|
-
function splitAnnotationsByRealm(text) {
|
|
5214
|
-
const units = parseAnnotationUnits(text);
|
|
5532
|
+
function splitAnnotationsByRealm(text, onProgress) {
|
|
5533
|
+
const units = parseAnnotationUnits(text, onProgress);
|
|
5215
5534
|
const classRealms = classRealmsOf(units);
|
|
5216
5535
|
const crossRealm = crossRealmClasses(units);
|
|
5217
5536
|
const memberRealms = markedMemberRealms(units);
|
|
5537
|
+
onProgress?.();
|
|
5218
5538
|
for (const unit of units) {
|
|
5219
5539
|
if (unit.owner) {
|
|
5220
5540
|
unit.realm = unit.owner;
|
|
@@ -5228,8 +5548,11 @@ function splitAnnotationsByRealm(text) {
|
|
|
5228
5548
|
const classRealm = unit.receiver ? classRealms.get(unit.receiver) : void 0;
|
|
5229
5549
|
unit.realm = classRealm && classRealm !== "shared" && !crossRealm.has(unit.receiver) ? classRealm : "shared";
|
|
5230
5550
|
}
|
|
5551
|
+
onProgress?.();
|
|
5552
|
+
const client = renderRealm(units, "client");
|
|
5553
|
+
onProgress?.();
|
|
5231
5554
|
return {
|
|
5232
|
-
client
|
|
5555
|
+
client,
|
|
5233
5556
|
server: renderRealm(units, "server")
|
|
5234
5557
|
};
|
|
5235
5558
|
}
|
|
@@ -5259,17 +5582,204 @@ function deriveRealmAnnotationFiles(annotationsPath, cacheDir) {
|
|
|
5259
5582
|
client: clientPath,
|
|
5260
5583
|
server: serverPath
|
|
5261
5584
|
};
|
|
5262
|
-
const
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
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
|
+
}
|
|
5267
5599
|
return {
|
|
5268
5600
|
client: clientPath,
|
|
5269
5601
|
server: serverPath
|
|
5270
5602
|
};
|
|
5271
5603
|
}
|
|
5272
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
|
|
5273
5783
|
//#region src/luals/version.ts
|
|
5274
5784
|
const FALLBACK_LUALS_VERSION = "3.19.1";
|
|
5275
5785
|
const DEFAULT_LUALS_VERSION = "latest";
|
|
@@ -5301,10 +5811,15 @@ async function fetchLatestLuaLSVersionFromGitHub() {
|
|
|
5301
5811
|
try {
|
|
5302
5812
|
const headers = { "User-Agent": "nanos-lint" };
|
|
5303
5813
|
if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
|
5304
|
-
const
|
|
5814
|
+
const result = await guardedFetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest", {
|
|
5305
5815
|
headers,
|
|
5306
5816
|
signal: AbortSignal.timeout(5e3)
|
|
5307
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;
|
|
5308
5823
|
if (res.ok) {
|
|
5309
5824
|
const data = await res.json();
|
|
5310
5825
|
const version = typeof data.tag_name === "string" ? sanitizeLuaLSVersion(data.tag_name) : null;
|
|
@@ -5853,6 +6368,13 @@ async function validateArchiveMembers(archivePath) {
|
|
|
5853
6368
|
* user-supplied `LUALS_BIN` may legitimately be a small wrapper script.
|
|
5854
6369
|
*/
|
|
5855
6370
|
const MIN_DOWNLOADED_BINARY_SIZE_BYTES = 1e5;
|
|
6371
|
+
/** Propagates blocked LuaLS execution or cache access without triggering repair. */
|
|
6372
|
+
function rethrowLuaLSPermissionError(err, targetPath, operation = "access") {
|
|
6373
|
+
if (err instanceof LuaLSError && (err.code === "ERR_LUALS_EXECUTION_DENIED" || err.code === "ERR_LUALS_CACHE_PERMISSION")) throw err;
|
|
6374
|
+
const code = err?.code;
|
|
6375
|
+
if (code !== "EPERM" && code !== "EACCES") return;
|
|
6376
|
+
throw new LuaLSError(`Cannot ${operation} LuaLS ${operation === "execute" ? "binary" : "cache path"} '${targetPath}': ${code}: ${err instanceof Error ? err.message : String(err)}`, operation === "execute" ? "ERR_LUALS_EXECUTION_DENIED" : "ERR_LUALS_CACHE_PERMISSION", operation === "execute" ? "Run nanos-lint in an environment that permits LuaLS execution and check the binary's execute permissions and sandbox policy. The existing cache has been preserved." : "Allow access to the LuaLS cache directory or use a writable cache directory.", { cause: err });
|
|
6377
|
+
}
|
|
5856
6378
|
/**
|
|
5857
6379
|
* Runs `binaryPath --version` and checks that it reports a LuaLS version.
|
|
5858
6380
|
* Shared by every binary validation path; the size heuristic stays separate
|
|
@@ -5867,6 +6389,7 @@ function reportsLuaLSVersion(binaryPath) {
|
|
|
5867
6389
|
});
|
|
5868
6390
|
return /^\d+\.\d+\.\d+/.test(output.trim());
|
|
5869
6391
|
} catch (err) {
|
|
6392
|
+
rethrowLuaLSPermissionError(err, binaryPath, "execute");
|
|
5870
6393
|
logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5871
6394
|
return false;
|
|
5872
6395
|
}
|
|
@@ -5882,6 +6405,7 @@ function isBinaryValid(binaryPath) {
|
|
|
5882
6405
|
if (!stats.isFile() || stats.size < 1e5) return false;
|
|
5883
6406
|
return reportsLuaLSVersion(binaryPath);
|
|
5884
6407
|
} catch (err) {
|
|
6408
|
+
rethrowLuaLSPermissionError(err, binaryPath);
|
|
5885
6409
|
logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5886
6410
|
return false;
|
|
5887
6411
|
}
|
|
@@ -5895,6 +6419,7 @@ function isBinaryRunnable(binaryPath) {
|
|
|
5895
6419
|
try {
|
|
5896
6420
|
if (!fs.statSync(binaryPath).isFile()) return false;
|
|
5897
6421
|
} catch (err) {
|
|
6422
|
+
rethrowLuaLSPermissionError(err, binaryPath);
|
|
5898
6423
|
logger.debug(`[luals] Binary validation check failed for ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5899
6424
|
return false;
|
|
5900
6425
|
}
|
|
@@ -5919,6 +6444,7 @@ function assertValidLuaLSBinary(binaryPath, source) {
|
|
|
5919
6444
|
try {
|
|
5920
6445
|
stats = fs.statSync(binaryPath);
|
|
5921
6446
|
} catch (err) {
|
|
6447
|
+
rethrowLuaLSPermissionError(err, binaryPath);
|
|
5922
6448
|
throw new LuaLSError(`${source} points to '${binaryPath}', which does not exist or cannot be read: ${err instanceof Error ? err.message : String(err)}`, "ERR_LUALS_BIN_INVALID", `Set ${source} to the full path of a lua-language-server executable, or leave it unset so nanos-lint resolves LuaLS automatically.`, { cause: err });
|
|
5923
6449
|
}
|
|
5924
6450
|
if (!stats.isFile()) throw new LuaLSError(`${source} points to '${binaryPath}', which is not a regular file (a directory or special file was found).`, "ERR_LUALS_BIN_INVALID", `Point ${source} at the lua-language-server executable itself, or leave it unset so nanos-lint resolves LuaLS automatically.`);
|
|
@@ -6035,11 +6561,13 @@ function listCachedLuaLSVersions(baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
|
6035
6561
|
if (fs.existsSync(marker) && fs.existsSync(bin)) try {
|
|
6036
6562
|
if (fs.readFileSync(marker, "utf-8").trim() === version && isBinaryValid(bin)) versions.push(version);
|
|
6037
6563
|
} catch (err) {
|
|
6564
|
+
rethrowLuaLSPermissionError(err, bin);
|
|
6038
6565
|
logger.debug(`[luals] Error validating cached version ${version}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6039
6566
|
}
|
|
6040
6567
|
}
|
|
6041
6568
|
return versions;
|
|
6042
6569
|
} catch (err) {
|
|
6570
|
+
rethrowLuaLSPermissionError(err, baseCacheDir);
|
|
6043
6571
|
logger.debug(`[luals] Failed to list cached LuaLS versions: ${err instanceof Error ? err.message : String(err)}`);
|
|
6044
6572
|
return [];
|
|
6045
6573
|
}
|
|
@@ -6089,6 +6617,7 @@ function findExistingLuaLSDir(version, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
|
6089
6617
|
if (fs.existsSync(primaryMarker)) try {
|
|
6090
6618
|
if (fs.readFileSync(primaryMarker, "utf-8").trim() === version && isBinaryValid(primaryBin)) return primaryCache;
|
|
6091
6619
|
} catch (err) {
|
|
6620
|
+
rethrowLuaLSPermissionError(err, primaryMarker);
|
|
6092
6621
|
logger.debug(`[luals] Error checking primary LuaLS cache marker at ${primaryMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6093
6622
|
}
|
|
6094
6623
|
const pkgRoot = getPackageRoot();
|
|
@@ -6100,20 +6629,6 @@ function findExistingLuaLSDir(version, baseCacheDir = getBaseLuaLSCacheDir()) {
|
|
|
6100
6629
|
//#region src/luals/download.ts
|
|
6101
6630
|
const DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
6102
6631
|
const MAX_ARCHIVE_SIZE_BYTES = 157286400;
|
|
6103
|
-
const ALLOWED_DOWNLOAD_DOMAINS = ["github.com", "githubusercontent.com"];
|
|
6104
|
-
/**
|
|
6105
|
-
* Validates that a download URL uses HTTPS and targets an allowlisted host.
|
|
6106
|
-
*/
|
|
6107
|
-
function isAllowedDownloadUrl(urlString) {
|
|
6108
|
-
try {
|
|
6109
|
-
const parsed = new URL(urlString);
|
|
6110
|
-
if (parsed.protocol !== "https:") return false;
|
|
6111
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
6112
|
-
return ALLOWED_DOWNLOAD_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
6113
|
-
} catch {
|
|
6114
|
-
return false;
|
|
6115
|
-
}
|
|
6116
|
-
}
|
|
6117
6632
|
/** Chunk size used when hashing an archive, so a 150 MB asset is never buffered whole. */
|
|
6118
6633
|
const HASH_CHUNK_SIZE_BYTES = 1048576;
|
|
6119
6634
|
/**
|
|
@@ -6148,12 +6663,22 @@ async function* limitDownloadStream(source) {
|
|
|
6148
6663
|
yield chunk;
|
|
6149
6664
|
}
|
|
6150
6665
|
}
|
|
6666
|
+
/** Reports cumulative transferred bytes while passing a download stream through unchanged. */
|
|
6667
|
+
async function* trackDownloadProgress(source, onProgress) {
|
|
6668
|
+
let transferred = 0;
|
|
6669
|
+
for await (const chunk of source) {
|
|
6670
|
+
transferred += chunk.length;
|
|
6671
|
+
onProgress(transferred);
|
|
6672
|
+
yield chunk;
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6151
6675
|
/** Returns whether a cache directory holds a complete, executable LuaLS installation. */
|
|
6152
6676
|
function isCompleteLuaLSInstall(binaryPath, completeMarker, version) {
|
|
6153
6677
|
if (!fs.existsSync(binaryPath) || !fs.existsSync(completeMarker)) return false;
|
|
6154
6678
|
try {
|
|
6155
6679
|
return fs.readFileSync(completeMarker, "utf-8").trim() === version && isBinaryValid(binaryPath);
|
|
6156
6680
|
} catch (err) {
|
|
6681
|
+
rethrowLuaLSPermissionError(err, completeMarker);
|
|
6157
6682
|
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6158
6683
|
return false;
|
|
6159
6684
|
}
|
|
@@ -6184,6 +6709,7 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
|
|
|
6184
6709
|
if (options?.reuseExisting !== false && fs.existsSync(binaryPath) && fs.existsSync(completeMarker)) try {
|
|
6185
6710
|
if (fs.readFileSync(completeMarker, "utf-8").trim() === resolvedVersion && isBinaryValid(binaryPath)) return binaryPath;
|
|
6186
6711
|
} catch (err) {
|
|
6712
|
+
rethrowLuaLSPermissionError(err, completeMarker);
|
|
6187
6713
|
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6188
6714
|
}
|
|
6189
6715
|
try {
|
|
@@ -6192,6 +6718,7 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
|
|
|
6192
6718
|
force: true
|
|
6193
6719
|
});
|
|
6194
6720
|
} catch (err) {
|
|
6721
|
+
rethrowLuaLSPermissionError(err, destDir);
|
|
6195
6722
|
logger.warn(`[luals] Failed to remove stale or invalid cache dir ${destDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6196
6723
|
}
|
|
6197
6724
|
}
|
|
@@ -6208,28 +6735,27 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
|
|
|
6208
6735
|
logger.info(`[luals] Reusing existing LuaLS ${resolvedVersion} installation from ${existingSourceDir}...`);
|
|
6209
6736
|
fs.cpSync(existingSourceDir, tempDir, { recursive: true });
|
|
6210
6737
|
} else {
|
|
6211
|
-
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.");
|
|
6212
6738
|
logger.info(`[luals] Downloading LuaLS ${resolvedVersion} from ${url}...`);
|
|
6213
6739
|
let response = null;
|
|
6214
6740
|
let lastErr = null;
|
|
6215
6741
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
6216
6742
|
try {
|
|
6217
|
-
const
|
|
6218
|
-
if (
|
|
6219
|
-
if (
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
if (
|
|
6226
|
-
response =
|
|
6743
|
+
const outcome = await guardedFetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
6744
|
+
if (!outcome.ok || !outcome.response) {
|
|
6745
|
+
if (outcome.reason !== "network-error") {
|
|
6746
|
+
const blockedMessage = `Redirect to untrusted URL blocked: ${outcome.url ?? url}`;
|
|
6747
|
+
logger.warn(`[luals] ${blockedMessage}`);
|
|
6748
|
+
throw new LuaLSError(blockedMessage, "ERR_LUALS_DOWNLOAD", "Download redirects must stay on allowlisted HTTPS GitHub hosts.");
|
|
6749
|
+
}
|
|
6750
|
+
lastErr = outcome.error ?? /* @__PURE__ */ new Error(`Failed to download ${url}`);
|
|
6751
|
+
} else if (outcome.response.ok && outcome.response.body) {
|
|
6752
|
+
response = outcome.response;
|
|
6227
6753
|
break;
|
|
6754
|
+
} else {
|
|
6755
|
+
const res = outcome.response;
|
|
6756
|
+
await cancelResponseBody(res);
|
|
6757
|
+
lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
|
|
6228
6758
|
}
|
|
6229
|
-
if (typeof res.body?.cancel === "function") try {
|
|
6230
|
-
await res.body.cancel();
|
|
6231
|
-
} catch (cancelErr) {}
|
|
6232
|
-
lastErr = /* @__PURE__ */ new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
|
|
6233
6759
|
} catch (err) {
|
|
6234
6760
|
if (err instanceof LuaLSError) throw err;
|
|
6235
6761
|
lastErr = err;
|
|
@@ -6245,50 +6771,76 @@ async function downloadAndPromoteLuaLS(resolvedVersion, info, destDir, binaryPat
|
|
|
6245
6771
|
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.");
|
|
6246
6772
|
}
|
|
6247
6773
|
}
|
|
6774
|
+
const declaredTotal = parseDeclaredContentLength(response);
|
|
6775
|
+
const downloadStartedAt = Date.now();
|
|
6776
|
+
let transferred = 0;
|
|
6777
|
+
const progress = createProgressBar({
|
|
6778
|
+
label: `[luals] Downloading ${info.assetName}`,
|
|
6779
|
+
total: declaredTotal,
|
|
6780
|
+
announce: false
|
|
6781
|
+
});
|
|
6248
6782
|
const fileStream = fs.createWriteStream(archivePath);
|
|
6249
6783
|
try {
|
|
6250
6784
|
const streamSource = typeof Readable.fromWeb === "function" && !("pipe" in response.body) ? Readable.fromWeb(response.body) : response.body;
|
|
6251
|
-
await pipeline(streamSource, limitDownloadStream,
|
|
6785
|
+
await pipeline(streamSource, limitDownloadStream, (source) => trackDownloadProgress(source, (bytes) => {
|
|
6786
|
+
transferred = bytes;
|
|
6787
|
+
progress.update(bytes, declaredTotal);
|
|
6788
|
+
}), fileStream);
|
|
6252
6789
|
} catch (streamErr) {
|
|
6790
|
+
progress.fail();
|
|
6253
6791
|
try {
|
|
6254
6792
|
if (fs.existsSync(archivePath)) fs.unlinkSync(archivePath);
|
|
6255
6793
|
} catch (unlinkErr) {}
|
|
6256
6794
|
if (streamErr instanceof LuaLSError) throw streamErr;
|
|
6257
6795
|
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 });
|
|
6258
6796
|
}
|
|
6797
|
+
progress.finish(`[luals] Downloaded ${info.assetName} (${formatTransferSummary(transferred, Date.now() - downloadStartedAt)})`);
|
|
6259
6798
|
const archiveSha256 = computeFileSha256(archivePath);
|
|
6260
6799
|
logger.info(`[luals] Downloaded ${info.assetName} (SHA-256 ${archiveSha256})`);
|
|
6261
6800
|
logger.info(`[luals] Extracting to ${destDir}...`);
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
tempDir
|
|
6269
|
-
] : [
|
|
6270
|
-
"-xf",
|
|
6271
|
-
path.basename(archivePath),
|
|
6272
|
-
"--no-same-owner",
|
|
6273
|
-
"--no-same-permissions",
|
|
6274
|
-
"-C",
|
|
6275
|
-
tempDir
|
|
6276
|
-
];
|
|
6277
|
-
await execFileAsync$1(getTarBinary(), tarArgs, { cwd: path.dirname(archivePath) });
|
|
6278
|
-
} catch (tarErr) {
|
|
6279
|
-
if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync$1("powershell.exe", [
|
|
6280
|
-
"-NoProfile",
|
|
6281
|
-
"-Command",
|
|
6282
|
-
`Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
|
|
6283
|
-
]);
|
|
6284
|
-
else throw tarErr;
|
|
6285
|
-
}
|
|
6286
|
-
const extractedSize = getDirectorySize(tempDir);
|
|
6287
|
-
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.");
|
|
6801
|
+
const spinner = createSpinner({
|
|
6802
|
+
label: `[luals] Extracting ${info.assetName}`,
|
|
6803
|
+
announce: false
|
|
6804
|
+
});
|
|
6805
|
+
spinner.start();
|
|
6806
|
+
let extractedSize;
|
|
6288
6807
|
try {
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6808
|
+
await validateArchiveMembers(archivePath);
|
|
6809
|
+
try {
|
|
6810
|
+
const tarArgs = process.platform === "win32" ? [
|
|
6811
|
+
"-xf",
|
|
6812
|
+
path.basename(archivePath),
|
|
6813
|
+
"-C",
|
|
6814
|
+
tempDir
|
|
6815
|
+
] : [
|
|
6816
|
+
"-xf",
|
|
6817
|
+
path.basename(archivePath),
|
|
6818
|
+
"--no-same-owner",
|
|
6819
|
+
"--no-same-permissions",
|
|
6820
|
+
"-C",
|
|
6821
|
+
tempDir
|
|
6822
|
+
];
|
|
6823
|
+
await execFileAsync$1(getTarBinary(), tarArgs, { cwd: path.dirname(archivePath) });
|
|
6824
|
+
} catch (tarErr) {
|
|
6825
|
+
if (process.platform === "win32" && info.assetName.endsWith(".zip")) await execFileAsync$1("powershell.exe", [
|
|
6826
|
+
"-NoProfile",
|
|
6827
|
+
"-Command",
|
|
6828
|
+
`Expand-Archive -Path '${escapePowerShellSingleQuote(archivePath)}' -DestinationPath '${escapePowerShellSingleQuote(tempDir)}' -Force`
|
|
6829
|
+
]);
|
|
6830
|
+
else throw tarErr;
|
|
6831
|
+
}
|
|
6832
|
+
spinner.updateText("[luals] Verifying extracted files");
|
|
6833
|
+
extractedSize = getDirectorySize(tempDir);
|
|
6834
|
+
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.");
|
|
6835
|
+
try {
|
|
6836
|
+
fs.unlinkSync(archivePath);
|
|
6837
|
+
} catch (err) {
|
|
6838
|
+
logger.debug(`[luals] Failed to delete temporary archive ${archivePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6839
|
+
}
|
|
6840
|
+
spinner.stop(`[luals] Extracted ${formatBytes(extractedSize)} to ${destDir}`);
|
|
6841
|
+
} catch (extractErr) {
|
|
6842
|
+
spinner.fail();
|
|
6843
|
+
throw extractErr;
|
|
6292
6844
|
}
|
|
6293
6845
|
}
|
|
6294
6846
|
const tempBinaryPath = path.join(tempDir, info.binaryRelativePath);
|
|
@@ -6775,6 +7327,7 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
6775
7327
|
if (options?.reuseExisting !== false) return cachedPath;
|
|
6776
7328
|
}
|
|
6777
7329
|
} catch (err) {
|
|
7330
|
+
rethrowLuaLSPermissionError(err, completeMarker);
|
|
6778
7331
|
logger.debug(`[luals] Failed to read complete marker at ${completeMarker}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6779
7332
|
}
|
|
6780
7333
|
if (!isValid) {
|
|
@@ -6787,6 +7340,7 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
6787
7340
|
force: true
|
|
6788
7341
|
});
|
|
6789
7342
|
} catch (err) {
|
|
7343
|
+
rethrowLuaLSPermissionError(err, cachedDir);
|
|
6790
7344
|
logger.warn(`[luals] Failed to remove corrupted cache directory ${cachedDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6791
7345
|
}
|
|
6792
7346
|
}
|
|
@@ -6796,11 +7350,13 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
6796
7350
|
const found = stdout.trim().split(/\r?\n/)[0];
|
|
6797
7351
|
if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
|
|
6798
7352
|
} catch (err) {
|
|
7353
|
+
rethrowLuaLSPermissionError(err, "lua-language-server", "execute");
|
|
6799
7354
|
logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
|
|
6800
7355
|
}
|
|
6801
7356
|
try {
|
|
6802
7357
|
return await downloadAndExtractLuaLS(resolvedVersion, getCacheDir(resolvedVersion, baseCacheDir), options);
|
|
6803
7358
|
} catch (err) {
|
|
7359
|
+
rethrowLuaLSPermissionError(err, cachedDir);
|
|
6804
7360
|
if (wasCorrupted) {
|
|
6805
7361
|
const isOffline = isOfflineError(err);
|
|
6806
7362
|
const reason = isOffline ? "while offline" : err instanceof Error ? err.message : String(err);
|
|
@@ -6862,12 +7418,14 @@ async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
|
|
|
6862
7418
|
const found = stdout.trim().split(/\r?\n/)[0];
|
|
6863
7419
|
if (found && fs.existsSync(found) && isBinaryValid(found)) return found;
|
|
6864
7420
|
} catch (err) {
|
|
7421
|
+
rethrowLuaLSPermissionError(err, "lua-language-server", "execute");
|
|
6865
7422
|
logger.debug(`[luals] LuaLS binary not found in PATH: ${err instanceof Error ? err.message : String(err)}`);
|
|
6866
7423
|
}
|
|
6867
7424
|
let downloadedBinary;
|
|
6868
7425
|
try {
|
|
6869
7426
|
downloadedBinary = await downloadAndExtractLuaLS(targetVersion, targetCacheDir, options);
|
|
6870
7427
|
} catch (err) {
|
|
7428
|
+
rethrowLuaLSPermissionError(err, targetCacheDir);
|
|
6871
7429
|
if (wasCorrupted) {
|
|
6872
7430
|
const isOffline = isOfflineError(err);
|
|
6873
7431
|
const reason = isOffline ? "while offline" : err instanceof Error ? err.message : String(err);
|
|
@@ -10555,7 +11113,7 @@ const DOCGEN_BRANCH = "docgen-output";
|
|
|
10555
11113
|
const RAW_ANNOTATIONS_URL = "https://raw.githubusercontent.com/nanos-world/vscode-extension/refs/heads/docgen-output/annotations.lua";
|
|
10556
11114
|
const GITHUB_COMMITS_API = "https://api.github.com/repos/nanos-world/vscode-extension/commits/docgen-output";
|
|
10557
11115
|
/** Reads an HTTP response body with an upper byte limit to guard against memory exhaustion. */
|
|
10558
|
-
async function readBoundedResponseBody(res, maxBytes, onExceeded) {
|
|
11116
|
+
async function readBoundedResponseBody(res, maxBytes, onExceeded, onProgress) {
|
|
10559
11117
|
const lengthHeader = res.headers?.get?.("content-length");
|
|
10560
11118
|
if (lengthHeader) {
|
|
10561
11119
|
const declared = parseInt(lengthHeader, 10);
|
|
@@ -10576,6 +11134,7 @@ async function readBoundedResponseBody(res, maxBytes, onExceeded) {
|
|
|
10576
11134
|
onExceeded(total, "stream");
|
|
10577
11135
|
return null;
|
|
10578
11136
|
}
|
|
11137
|
+
onProgress?.(total);
|
|
10579
11138
|
chunks.push(buf);
|
|
10580
11139
|
}
|
|
10581
11140
|
return Buffer.concat(chunks).toString("utf-8");
|
|
@@ -10591,15 +11150,35 @@ async function readBoundedResponseBody(res, maxBytes, onExceeded) {
|
|
|
10591
11150
|
}
|
|
10592
11151
|
return "";
|
|
10593
11152
|
}
|
|
11153
|
+
/**
|
|
11154
|
+
* Resolves a request through the download guard, which refuses an off-allowlist request URL,
|
|
11155
|
+
* an off-allowlist redirect hop and an off-allowlist final response URL, releasing the body of
|
|
11156
|
+
* every refused response instead of downloading it.
|
|
11157
|
+
*/
|
|
11158
|
+
async function resolveGuardedResponse(url, init) {
|
|
11159
|
+
const result = await guardedFetch(url, init);
|
|
11160
|
+
if (result.ok && result.response) return { res: result.response };
|
|
11161
|
+
return { block: result };
|
|
11162
|
+
}
|
|
11163
|
+
/** Builds the user-facing error for a request refused by the download transport policy. */
|
|
11164
|
+
function downloadBlockError(result) {
|
|
11165
|
+
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 });
|
|
11166
|
+
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://.");
|
|
11167
|
+
}
|
|
10594
11168
|
/** Fetches the latest commit SHA for the annotations branch from the GitHub API. */
|
|
10595
11169
|
async function fetchLatestCommitId() {
|
|
10596
11170
|
try {
|
|
10597
11171
|
const headers = { "User-Agent": "nanos-lint" };
|
|
10598
11172
|
if (process.env.GITHUB_TOKEN) headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
|
10599
|
-
const
|
|
11173
|
+
const outcome = await resolveGuardedResponse(GITHUB_COMMITS_API, {
|
|
10600
11174
|
headers,
|
|
10601
11175
|
signal: AbortSignal.timeout(5e3)
|
|
10602
11176
|
});
|
|
11177
|
+
if ("block" in outcome) {
|
|
11178
|
+
if (outcome.block.reason !== "network-error") logger.warn(`[annotations] ${downloadBlockError(outcome.block).message}`);
|
|
11179
|
+
return null;
|
|
11180
|
+
}
|
|
11181
|
+
const { res } = outcome;
|
|
10603
11182
|
if (res.ok) {
|
|
10604
11183
|
const rawJson = await readBoundedResponseBody(res, MAX_COMMIT_JSON_SIZE_BYTES, (bytes) => {
|
|
10605
11184
|
logger.warn(`GitHub commits response exceeded size limit of ${MAX_COMMIT_JSON_SIZE_BYTES} bytes (${bytes} bytes)`);
|
|
@@ -10626,16 +11205,40 @@ function getRawAnnotationsUrl(commitSha) {
|
|
|
10626
11205
|
}
|
|
10627
11206
|
/** Downloads raw annotations.lua content from GitHub raw content endpoints. */
|
|
10628
11207
|
async function fetchRawAnnotationsContent(commitSha) {
|
|
10629
|
-
const
|
|
10630
|
-
const res = await fetch(url, {
|
|
11208
|
+
const outcome = await resolveGuardedResponse(getRawAnnotationsUrl(commitSha), {
|
|
10631
11209
|
headers: { "User-Agent": "nanos-lint" },
|
|
10632
11210
|
signal: AbortSignal.timeout(15e3)
|
|
10633
11211
|
});
|
|
11212
|
+
if ("block" in outcome) {
|
|
11213
|
+
const blockError = downloadBlockError(outcome.block);
|
|
11214
|
+
logger.warn(`[annotations] ${blockError.message}`);
|
|
11215
|
+
throw blockError;
|
|
11216
|
+
}
|
|
11217
|
+
const { res } = outcome;
|
|
10634
11218
|
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.");
|
|
10635
|
-
const
|
|
10636
|
-
|
|
11219
|
+
const declaredTotal = parseDeclaredContentLength(res);
|
|
11220
|
+
const startedAt = Date.now();
|
|
11221
|
+
let transferred = 0;
|
|
11222
|
+
const commitLabel = commitSha ? ` (${commitSha.slice(0, 7)})` : "";
|
|
11223
|
+
const progress = createProgressBar({
|
|
11224
|
+
label: `[annotations] Downloading ${ANNOTATIONS_FILENAME}${commitLabel}`,
|
|
11225
|
+
total: declaredTotal,
|
|
11226
|
+
announce: false
|
|
10637
11227
|
});
|
|
10638
|
-
|
|
11228
|
+
let text;
|
|
11229
|
+
try {
|
|
11230
|
+
text = await readBoundedResponseBody(res, MAX_ANNOTATIONS_SIZE_BYTES, (bytes, reason) => {
|
|
11231
|
+
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>.");
|
|
11232
|
+
}, (bytes) => {
|
|
11233
|
+
transferred = bytes;
|
|
11234
|
+
progress.update(bytes, declaredTotal);
|
|
11235
|
+
});
|
|
11236
|
+
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>.");
|
|
11237
|
+
} catch (err) {
|
|
11238
|
+
progress.fail();
|
|
11239
|
+
throw err;
|
|
11240
|
+
}
|
|
11241
|
+
progress.finish(`[annotations] Downloaded ${ANNOTATIONS_FILENAME} (${formatTransferSummary(transferred, Date.now() - startedAt)})`);
|
|
10639
11242
|
return text;
|
|
10640
11243
|
}
|
|
10641
11244
|
/**
|
|
@@ -10882,11 +11485,12 @@ function createProgram(options) {
|
|
|
10882
11485
|
"info",
|
|
10883
11486
|
"debug",
|
|
10884
11487
|
"silent"
|
|
10885
|
-
]).default(DEFAULT_LOG_LEVEL)).hook("preAction", (thisCommand, actionCommand) => {
|
|
11488
|
+
]).default(DEFAULT_LOG_LEVEL)).option("--no-progress", "Disable the interactive download, extraction and realm derivation progress display").hook("preAction", (thisCommand, actionCommand) => {
|
|
10886
11489
|
const target = actionCommand || thisCommand;
|
|
10887
11490
|
const opts = target.optsWithGlobals ? target.optsWithGlobals() : target.opts();
|
|
10888
11491
|
if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
|
|
10889
|
-
|
|
11492
|
+
if (opts.progress === false) setProgressMode("off");
|
|
11493
|
+
}).exitOverride().configureHelp({ showGlobalOptions: true }).configureOutput({
|
|
10890
11494
|
writeOut: (str) => console.log(str.trimEnd()),
|
|
10891
11495
|
writeErr: (str) => logger.error(str.trimEnd())
|
|
10892
11496
|
});
|
|
@@ -10914,6 +11518,10 @@ function createProgram(options) {
|
|
|
10914
11518
|
if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
|
|
10915
11519
|
const { rootPath, targetPaths: canonicalTargets } = resolveCheckTargets(targetPaths && targetPaths.length > 0 ? targetPaths : ["."]);
|
|
10916
11520
|
const format = opts.github ? "github" : opts.format || (process.env.GITHUB_ACTIONS ? "github" : "pretty");
|
|
11521
|
+
if (format === "json") {
|
|
11522
|
+
setProgressMode("off");
|
|
11523
|
+
logger.setDiagnosticStream("stderr");
|
|
11524
|
+
}
|
|
10917
11525
|
const checkOptions = {
|
|
10918
11526
|
path: rootPath,
|
|
10919
11527
|
paths: canonicalTargets,
|
|
@@ -11005,6 +11613,7 @@ function createProgram(options) {
|
|
|
11005
11613
|
}
|
|
11006
11614
|
};
|
|
11007
11615
|
const handleCacheStatus = (opts) => {
|
|
11616
|
+
if (opts?.json) logger.setDiagnosticStream("stderr");
|
|
11008
11617
|
const report = getCacheStatus();
|
|
11009
11618
|
if (opts?.json) writeOutput(JSON.stringify(report, null, 2));
|
|
11010
11619
|
else writeOutput(formatCacheStatusPretty(report));
|
|
@@ -11046,6 +11655,8 @@ Examples:
|
|
|
11046
11655
|
}
|
|
11047
11656
|
/** Parses command-line arguments and executes the requested CLI action. */
|
|
11048
11657
|
async function runCLI(args = process.argv.slice(2)) {
|
|
11658
|
+
setProgressMode("auto");
|
|
11659
|
+
logger.setDiagnosticStream("stdout");
|
|
11049
11660
|
for (let i = 0; i < args.length; i++) {
|
|
11050
11661
|
const arg = args[i];
|
|
11051
11662
|
if (!arg) continue;
|
|
@@ -11118,6 +11729,6 @@ if (isDirectExecution()) runCLI().then((code) => {
|
|
|
11118
11729
|
process.exit(1);
|
|
11119
11730
|
});
|
|
11120
11731
|
//#endregion
|
|
11121
|
-
export {
|
|
11732
|
+
export { DOWNLOAD_TIMEOUT_MS as $, buildWorkspaceConfig as $t, formatProblemSummary as A, parseLogLevel as An, getPlatformInfo as At, resolvePackageDependencies as B, splitAnnotationsByRealm as Bt, getCachedAnnotationsFilePath as C, fileUriToPath as Cn, isBinaryRunnable as Ct, updateLastCheckedDate as D, Logger as Dn, parseTarTvSize as Dt, readAnnotationsMetadata as E, LOG_LEVEL_PRIORITY as En, isZipArchive as Et, shouldEnableColor as F, resolveLuaLSVersion as Ft, computeUnrequestedExclusions as G, createSpinner as Gt, runLuaLSCheck as H, PROGRESS_THROTTLE_MS as Ht, collectRealmFiles as I, sanitizeLuaLSVersion as It, findProjectRoot as J, isProgressInteractive as Jt, filterReportByTargetPaths as K, formatTransferSummary as Kt, planRealmCheck as L, ALLOWED_DOWNLOAD_DOMAINS as Lt, formatSeverityBadge as M, FALLBACK_LUALS_VERSION as Mt, getColors as N, fetchLatestLuaLSVersionFromGitHub as Nt, formatGitHubAnnotations as O, isValidLogLevel as On, rethrowLuaLSPermissionError as Ot, pluralize as P, resolveLatestLuaLSVersion as Pt, resolveCheckTargets as Q, VALID_LUALS_DIAGNOSTIC_CODES as Qt, runRealmAwareCheck as R, isAllowedDownloadUrl as Rt, getAnnotationsMetadataFilePath as S, systemPaths as Sn, inspectZipMembers as St, isAnnotationsValid as T, LOG_LEVELS as Tn, isGzipArchive as Tt, countCheckedFiles as U, SPINNER_FRAMES as Ut, resolveLuaLSBinary as V, PROGRESS_MILESTONES as Vt, listCheckedFiles as W, createProgressBar as Wt, isFilesystemRoot as X, setProgressMode as Xt, getCanonicalPath as Y, restoreTerminal as Yt, matchesTargetPaths as Z, DEFAULT_REALM_MAPPINGS as Zt, MAX_ANNOTATIONS_SIZE_BYTES as _, LuaLSError as _n, MAX_DECOMPRESSED_SIZE_BYTES as _t, formatCacheStatusPretty as a, loadConfigFile as an, LUALS_METADATA_FILENAME as at, MIN_ANNOTATIONS_SIZE_BYTES as b, formatBytes as bn, escapePowerShellSingleQuote as bt, DOCGEN_BRANCH as c, parseJsonc as cn, getBaseLuaLSCacheDir as ct, RAW_ANNOTATIONS_URL as d, stripJsonComments as dn, getLuaLSMetadataPath as dt, getDefaultAnnotationsPath as en, MAX_ARCHIVE_SIZE_BYTES as et, downloadAndCacheAnnotations as f, stripTrailingSlashes as fn, listCachedLuaLSVersionDirs as ft, ANNOTATIONS_FILENAME as g, ConfigError as gn, MAX_ARCHIVE_MEMBER_COUNT as gt, getRawAnnotationsUrl as h, CacheError as hn, writeLuaLSMetadata as ht, runCLI as i, initWorkspace as in, trackDownloadProgress as it, formatReport as j, DEFAULT_LUALS_VERSION as jt, formatPretty as k, logger as kn, validateArchiveMembers as kt, DOCGEN_REPO as l, resolveRealmMappings as ln, getCacheDir as lt, fetchRawAnnotationsContent as m, AnnotationsError as mn, readLuaLSMetadata as mt, createProgram as n, getDefinitionsDir as nn, downloadAndExtractLuaLS as nt, getCacheStatus as o, loadUserConfig as on, cleanupOldCachedLuaLSVersions as ot, fetchLatestCommitId as p, writeTempConfig as pn, listCachedLuaLSVersions as pt, findCommonAncestorDirectory as q, getProgressMode as qt, isDirectExecution as r, getPackageRoot as rn, limitDownloadStream as rt, resolveAnnotations as s, mergeConfigs as sn, findExistingLuaLSDir as st, collectIgnorePatterns as t, getDefaultTemplatePath as tn, computeFileSha256 as tt, GITHUB_COMMITS_API as u, resolveWorkspaceConfig as un, getIsoWeek as ut, MAX_COMMIT_JSON_SIZE_BYTES as v, NanosLintError as vn, MIN_DOWNLOADED_BINARY_SIZE_BYTES as vt, getTodayDateString as w, DEFAULT_LOG_LEVEL as wn, isBinaryValid as wt, getAnnotationsCacheDir as x, getDirectorySize as xn, getTarBinary as xt, METADATA_FILENAME as y, cleanCache as yn, assertValidLuaLSBinary as yt, collectDeps as z, deriveRealmAnnotationFiles as zt };
|
|
11122
11733
|
|
|
11123
|
-
//# sourceMappingURL=cli-
|
|
11734
|
+
//# sourceMappingURL=cli-I16SeKRK.js.map
|