nanos-lint 3.2.1 → 3.4.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.
@@ -1464,20 +1464,23 @@ function resolveRealmMappings(userConfig) {
1464
1464
  function initWorkspace(workspacePath, options) {
1465
1465
  const targetFile = path.join(workspacePath, ".luarc.json");
1466
1466
  if (fs.existsSync(targetFile) && !options?.force) throw new ConfigError(`.luarc.json already exists at ${targetFile}. Use --force to overwrite.`, "ERR_CONFIG_EXISTS", "Pass --force to overwrite the existing .luarc.json file.");
1467
- const template = loadConfigFile(getDefaultTemplatePath());
1468
- const sourceAnnotations = options?.annotationsPath || getDefaultAnnotationsPath();
1469
- if (!fs.existsSync(sourceAnnotations)) throw new ConfigError(`Definitions file not found at ${sourceAnnotations}. Make sure annotations have been downloaded or pass a valid file with --annotations.`, "ERR_ANNOTATIONS_NOT_FOUND", "Run 'nanos-lint warmup' to download annotations or provide --annotations <path>.");
1470
- const targetNanosDir = path.join(workspacePath, ".nanos-lint");
1471
- fs.mkdirSync(targetNanosDir, { recursive: true });
1472
- const targetAnnotations = path.join(targetNanosDir, "annotations.lua");
1473
- fs.copyFileSync(sourceAnnotations, targetAnnotations);
1474
- template.workspace = template.workspace ?? {};
1475
- template.workspace.library = [".nanos-lint/annotations.lua"];
1476
- const existingIgnore = template.workspace.ignoreDir ?? [];
1477
- if (!existingIgnore.includes(".nanos-lint")) template.workspace.ignoreDir = [".nanos-lint", ...existingIgnore];
1478
- template.files = template.files ?? {};
1479
- const existingExclude = template.files.exclude ?? [];
1480
- if (!existingExclude.includes(".nanos-lint/**")) template.files.exclude = [".nanos-lint/**", ...existingExclude];
1467
+ if (options?.annotationsPath && !options.vendor) throw new ConfigError("--annotations requires --vendor: nanos-lint init does not pin or vendor annotations.lua by default.", "ERR_ANNOTATIONS_WITHOUT_VENDOR", "Run 'nanos-lint init --vendor --annotations <path>' (or pass vendor: true), or omit the custom path to use the managed annotations cache.");
1468
+ const template = loadConfigFile(path.resolve(options?.templatePath ?? getDefaultTemplatePath()));
1469
+ if (!template || typeof template !== "object" || Array.isArray(template)) throw new ConfigError("Init template must contain a configuration object.", "ERR_CONFIG_PARSE");
1470
+ template.nanos ??= {};
1471
+ template.nanos.realms ??= Object.fromEntries(DEFAULT_REALM_MAPPINGS.map(({ pattern, realm }) => [pattern, realm]));
1472
+ if (options?.vendor) {
1473
+ const sourceAnnotations = options.annotationsPath || getDefaultAnnotationsPath();
1474
+ if (!fs.existsSync(sourceAnnotations)) throw new ConfigError(`Definitions file not found at ${sourceAnnotations}. Make sure annotations have been downloaded or pass a valid file with --annotations.`, "ERR_ANNOTATIONS_NOT_FOUND", "Run 'nanos-lint warmup' to download annotations or provide --annotations <path>.");
1475
+ const targetAnnotations = path.join(workspacePath, ".nanos-lint", "annotations.lua");
1476
+ fs.mkdirSync(path.dirname(targetAnnotations), { recursive: true });
1477
+ fs.copyFileSync(sourceAnnotations, targetAnnotations);
1478
+ template.workspace = template.workspace ?? {};
1479
+ template.workspace.library = [.../* @__PURE__ */ new Set([...template.workspace.library ?? [], ".nanos-lint/annotations.lua"])];
1480
+ template.workspace.ignoreDir = [.../* @__PURE__ */ new Set([".nanos-lint", ...template.workspace.ignoreDir ?? []])];
1481
+ template.files = template.files ?? {};
1482
+ template.files.exclude = [.../* @__PURE__ */ new Set([".nanos-lint/**", ...template.files.exclude ?? []])];
1483
+ }
1481
1484
  fs.writeFileSync(targetFile, JSON.stringify(template, null, 2), "utf-8");
1482
1485
  return targetFile;
1483
1486
  }
@@ -7132,6 +7135,40 @@ function resolveCheckTargets(rawPaths) {
7132
7135
  };
7133
7136
  }
7134
7137
  //#endregion
7138
+ //#region src/luals/offline.ts
7139
+ /** Validates an explicit local executable and checks a requested version against its output. */
7140
+ function validateOfflineBinary(binary, version) {
7141
+ const resolved = assertValidLuaLSBinary(binary, "offline local binary");
7142
+ if (version !== "latest") {
7143
+ if (!execFileSync(resolved, ["--version"], {
7144
+ encoding: "utf-8",
7145
+ timeout: 5e3
7146
+ }).split(/\s+/).some((part) => sanitizeLuaLSVersion(part) === version)) throw new LuaLSError(`Offline LuaLS binary ${resolved} does not match requested version ${version}.`, "ERR_LUALS_OFFLINE", `Run 'nanos-lint warmup --luals-version ${version}' online or set LUALS_BIN to a matching binary.`);
7147
+ }
7148
+ return resolved;
7149
+ }
7150
+ /** Resolves local LuaLS assets without changing cache entries or discovering remote releases. */
7151
+ function resolveOfflineLuaLS(version, cacheDir = getBaseLuaLSCacheDir()) {
7152
+ const requested = version === "latest" ? version : sanitizeLuaLSVersion(version);
7153
+ if (!requested) throw new LuaLSError(`Invalid LuaLS version: "${version}".`, "ERR_LUALS_INVALID_VERSION");
7154
+ if (process.env.LUALS_BIN) return validateOfflineBinary(process.env.LUALS_BIN, requested);
7155
+ const versions = listCachedLuaLSVersions(cacheDir).sort((a, b) => b.localeCompare(a, "en", { numeric: true }));
7156
+ const selected = requested === "latest" ? versions[0] : versions.find((v) => v === requested);
7157
+ if (selected) {
7158
+ logger.info(`[luals] Offline: using cached LuaLS ${selected}.`);
7159
+ return path.join(getCacheDir(selected, cacheDir), getPlatformInfo(selected).binaryRelativePath);
7160
+ }
7161
+ const bundled = path.join(getPackageRoot(), getPlatformInfo(FALLBACK_LUALS_VERSION).binaryRelativePath);
7162
+ if (fs.existsSync(bundled)) return validateOfflineBinary(bundled, requested);
7163
+ try {
7164
+ const found = execFileSync(process.platform === "win32" ? "where.exe" : "which", ["lua-language-server"], { encoding: "utf-8" }).trim().split(/\r?\n/)[0];
7165
+ if (found) return validateOfflineBinary(found, requested);
7166
+ } catch (err) {
7167
+ rethrowLuaLSPermissionError(err, "lua-language-server", "execute");
7168
+ }
7169
+ throw new LuaLSError(`Offline: no usable local LuaLS binary for ${requested}.`, "ERR_LUALS_OFFLINE", `Run 'nanos-lint warmup${requested === "latest" ? "" : ` --luals-version ${requested}`}' online or set LUALS_BIN to a matching local executable.`);
7170
+ }
7171
+ //#endregion
7135
7172
  //#region src/luals/files.ts
7136
7173
  /** Directories skipped unless the workspace config overrides `workspace.ignoreDir`. */
7137
7174
  const DEFAULT_IGNORE_DIRS = [
@@ -7300,6 +7337,7 @@ function isOfflineError(err) {
7300
7337
  }
7301
7338
  /** Resolves the executable path of a LuaLS binary from env, bundle, cache, or network download. */
7302
7339
  async function resolveLuaLSBinary(version = DEFAULT_LUALS_VERSION, options) {
7340
+ if (options?.offline) return resolveOfflineLuaLS(version, options.cacheDir);
7303
7341
  if (process.env.LUALS_BIN) return assertValidLuaLSBinary(process.env.LUALS_BIN, "LUALS_BIN");
7304
7342
  if (options?.reuseExisting !== false && (!version || version === "latest")) {
7305
7343
  const defaultInfo = getPlatformInfo(FALLBACK_LUALS_VERSION);
@@ -7445,7 +7483,7 @@ async function runLuaLSCheck(targetPath, configPath, options) {
7445
7483
  logger.debug(`[luals] Failed to resolve native realpath for target: ${err instanceof Error ? err.message : String(err)}`);
7446
7484
  }
7447
7485
  if (!fs.existsSync(absoluteTarget)) throw new LuaLSError(`Target path does not exist: ${targetPath}`, "ERR_TARGET_NOT_FOUND", "Verify that the target path exists and is accessible.");
7448
- const binary = options.lualsBin ? assertValidLuaLSBinary(options.lualsBin, "--luals-bin") : await resolveLuaLSBinary(options.lualsVersion);
7486
+ const binary = options.lualsBin ? options.offline ? validateOfflineBinary(options.lualsBin, options.lualsVersion ?? "latest") : assertValidLuaLSBinary(options.lualsBin, "--luals-bin") : await resolveLuaLSBinary(options.lualsVersion, { offline: options.offline });
7449
7487
  let checkDir = absoluteTarget;
7450
7488
  let targetFileOnly = null;
7451
7489
  if (fs.statSync(absoluteTarget).isFile()) {
@@ -7987,7 +8025,7 @@ function formatPretty(result, cwd = process.cwd(), useColor = shouldEnableColor(
7987
8025
  const c = getColors(useColor);
7988
8026
  const symCross = "✖ ";
7989
8027
  const symCheck = "✔ ";
7990
- if (result.passed) {
8028
+ if (result.passed && !result.baselineSummary) {
7991
8029
  const fileStr = pluralize(result.totalFilesChecked ?? result.totalFiles, "file");
7992
8030
  return `${c.green}${c.bold}${symCheck}Diagnosis completed, no problems found across ${fileStr}.${c.reset}`;
7993
8031
  }
@@ -8003,6 +8041,7 @@ function formatPretty(result, cwd = process.cwd(), useColor = shouldEnableColor(
8003
8041
  logger.debug(`Failed to read file snippet for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
8004
8042
  }
8005
8043
  for (const d of diags) {
8044
+ if (d.baseline) continue;
8006
8045
  const line = d.range.start.line + 1;
8007
8046
  const col = d.range.start.character + 1;
8008
8047
  const badge = formatSeverityBadge(d.severity, useColor);
@@ -8028,8 +8067,10 @@ function formatPretty(result, cwd = process.cwd(), useColor = shouldEnableColor(
8028
8067
  else if (d.severity === 2) warnings++;
8029
8068
  }
8030
8069
  lines.push("");
8031
- const summary = formatProblemSummary(result.totalProblems, errors, warnings, result.totalFiles);
8032
- lines.push(`${c.red}${c.bold}${symCross}${summary}${c.reset}`);
8070
+ const counts = result.baselineSummary;
8071
+ const summary = counts ? `Diagnosis complete: ${counts.new} new, ${counts.baselined} baselined, ${counts.total} total (${counts.stale} stale baseline entries).` : formatProblemSummary(result.totalProblems, errors, warnings, result.totalFiles);
8072
+ const color = result.passed ? c.green : c.red;
8073
+ lines.push(`${color}${c.bold}${result.passed ? symCheck : symCross}${summary}${c.reset}`);
8033
8074
  return lines.join("\n");
8034
8075
  }
8035
8076
  /** Formats check diagnostics into GitHub Actions workflow annotation commands. */
@@ -8040,6 +8081,7 @@ function formatGitHubAnnotations(result, cwd = process.cwd()) {
8040
8081
  const filePath = fileUriToPath(rawUri);
8041
8082
  const escapedFile = (path.isAbsolute(filePath) ? path.relative(cwd, filePath).replace(/\\/g, "/") : filePath.replace(/\\/g, "/")).replace(/%/g, "%25").replace(/,/g, "%2C");
8042
8083
  for (const d of diags) {
8084
+ if (d.baseline) continue;
8043
8085
  const line = d.range.start.line + 1;
8044
8086
  const col = d.range.start.character + 1;
8045
8087
  const endLine = d.range.end.line + 1;
@@ -11316,6 +11358,10 @@ async function resolveAnnotations(options = {}) {
11316
11358
  if (fs.existsSync(bundled) && isAnnotationsValid(bundled)) return bundled;
11317
11359
  const cacheDir = options.cacheDir ?? getAnnotationsCacheDir();
11318
11360
  const cachedAnnotationsFile = path.join(cacheDir, ANNOTATIONS_FILENAME);
11361
+ if (options.offline) {
11362
+ if (isAnnotationsValid(cachedAnnotationsFile)) return cachedAnnotationsFile;
11363
+ throw new AnnotationsError(`Offline: no usable local annotations.lua at ${cachedAnnotationsFile}.`, "ERR_ANNOTATIONS_OFFLINE", "Run 'nanos-lint warmup' online or specify --annotations <path>.");
11364
+ }
11319
11365
  const metadata = readAnnotationsMetadata(cacheDir);
11320
11366
  const { dateStr } = getTodayDateString();
11321
11367
  if (fs.existsSync(cachedAnnotationsFile) && !isAnnotationsValid(cachedAnnotationsFile)) {
@@ -11361,6 +11407,25 @@ async function resolveAnnotations(options = {}) {
11361
11407
  throw new AnnotationsError(`Failed to resolve nanos world API annotations. Please check your network connection or pass a custom file with --annotations. (${err instanceof Error ? err.message : String(err)})`, "ERR_ANNOTATIONS_NETWORK", "Check your network connection, run 'nanos-lint warmup' when online, or specify --annotations <path>.", { cause: err });
11362
11408
  }
11363
11409
  }
11410
+ const DEFAULT_COPY_TARGET = `.nanos-lint/${ANNOTATIONS_FILENAME}`;
11411
+ /**
11412
+ * Copies nanos world annotations.lua definitions into a target file or directory.
11413
+ * A target counts as a directory when it ends with a separator, already is a directory,
11414
+ * or does not end in `.lua` (case-insensitively).
11415
+ */
11416
+ async function copyAnnotations(targetPath = DEFAULT_COPY_TARGET, options = {}) {
11417
+ let resolvedTarget = path.resolve(targetPath);
11418
+ if (targetPath.endsWith("/") || targetPath.endsWith("\\") || fs.existsSync(resolvedTarget) && fs.statSync(resolvedTarget).isDirectory() || path.extname(resolvedTarget).toLowerCase() !== ".lua") resolvedTarget = path.join(resolvedTarget, ANNOTATIONS_FILENAME);
11419
+ if (fs.existsSync(resolvedTarget) && !options.force) throw new AnnotationsError(`Target annotations file already exists: ${resolvedTarget}. Use --force to overwrite.`, "ERR_ANNOTATIONS_EXISTS", "Pass --force to overwrite the existing annotations file.");
11420
+ const sourcePath = options.annotationsPath ? validateCustomAnnotationsPath(options.annotationsPath, "custom") : await resolveAnnotations({
11421
+ cacheDir: options.cacheDir,
11422
+ offline: options.offline
11423
+ });
11424
+ const targetDir = path.dirname(resolvedTarget);
11425
+ fs.mkdirSync(targetDir, { recursive: true });
11426
+ fs.copyFileSync(sourcePath, resolvedTarget);
11427
+ return resolvedTarget;
11428
+ }
11364
11429
  //#endregion
11365
11430
  //#region src/cache-status.ts
11366
11431
  /** Collects detailed status and disk usage information for LuaLS and annotations caches. */
@@ -11455,6 +11520,190 @@ function formatCacheStatusPretty(report) {
11455
11520
  return lines.join("\n");
11456
11521
  }
11457
11522
  //#endregion
11523
+ //#region src/output.ts
11524
+ /** Writes command results to stdout unless the log level is `silent`. */
11525
+ function writeOutput(message) {
11526
+ if (logger.isOutputEnabled()) console.log(message);
11527
+ }
11528
+ //#endregion
11529
+ //#region src/baseline.ts
11530
+ /** Builds a line-independent diagnostic identity relative to the checked workspace root. */
11531
+ function entryFor(root, uri, diagnostic) {
11532
+ const file = fileUriToPath(uri);
11533
+ return {
11534
+ path: path.relative(root, path.isAbsolute(file) ? file : path.resolve(root, file)).replace(/\\/g, "/"),
11535
+ code: diagnostic.code ?? "",
11536
+ message: diagnostic.message.replace(/\s+/g, " ").trim(),
11537
+ severity: diagnostic.severity,
11538
+ count: 1
11539
+ };
11540
+ }
11541
+ /** Serializes the fields defining a diagnostic identity, excluding occurrence count. */
11542
+ function keyFor(entry) {
11543
+ return JSON.stringify([
11544
+ entry.path,
11545
+ entry.code,
11546
+ entry.message,
11547
+ entry.severity
11548
+ ]);
11549
+ }
11550
+ /** Records sorted diagnostic identities and multiplicities without source positions. */
11551
+ function createBaseline(result, root) {
11552
+ const entries = /* @__PURE__ */ new Map();
11553
+ for (const [uri, diagnostics] of Object.entries(result.diagnostics)) for (const diagnostic of diagnostics) {
11554
+ const entry = entryFor(root, uri, diagnostic);
11555
+ const key = keyFor(entry);
11556
+ const previous = entries.get(key);
11557
+ if (previous) previous.count++;
11558
+ else entries.set(key, entry);
11559
+ }
11560
+ return {
11561
+ version: 1,
11562
+ entries: [...entries.values()].sort((a, b) => {
11563
+ const left = keyFor(a);
11564
+ const right = keyFor(b);
11565
+ return left < right ? -1 : left > right ? 1 : 0;
11566
+ })
11567
+ };
11568
+ }
11569
+ /** Validates a persisted baseline and rejects ambiguous or incompatible entries. */
11570
+ function readBaseline(file) {
11571
+ try {
11572
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
11573
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) throw new Error("Unsupported baseline schema (expected version 1).");
11574
+ const keys = /* @__PURE__ */ new Set();
11575
+ for (const entry of parsed.entries) {
11576
+ if (!entry || typeof entry.path !== "string" || !entry.path || typeof entry.code !== "string" || typeof entry.message !== "string" || !Number.isInteger(entry.severity) || entry.severity < 1 || entry.severity > 4 || !Number.isSafeInteger(entry.count) || entry.count < 1) throw new Error("Invalid baseline diagnostic entry.");
11577
+ const key = keyFor(entry);
11578
+ if (keys.has(key)) throw new Error("Duplicate baseline diagnostic entry.");
11579
+ keys.add(key);
11580
+ }
11581
+ return parsed;
11582
+ } catch (cause) {
11583
+ throw new ConfigError(`Cannot read diagnostic baseline ${file}: ${cause instanceof Error ? cause.message : String(cause)}`, "ERR_BASELINE_READ", "Regenerate with 'nanos-lint check <targets> --write-baseline <path>' using the same targets and options.", { cause });
11584
+ }
11585
+ }
11586
+ /** Atomically writes a deterministic baseline, creating its parent directory if needed. */
11587
+ function writeBaseline(file, result, root) {
11588
+ try {
11589
+ writeAtomicFileSync(path.resolve(file), `${JSON.stringify(createBaseline(result, root), null, 2)}\n`);
11590
+ } catch (cause) {
11591
+ throw new ConfigError(`Cannot write diagnostic baseline ${file}: ${cause instanceof Error ? cause.message : String(cause)}`, "ERR_BASELINE_WRITE", "Choose a writable baseline path.", { cause });
11592
+ }
11593
+ }
11594
+ /** Classifies each diagnostic against an occurrence budget and computes strict staleness. */
11595
+ function compareBaseline(result, baseline, root, strict = false) {
11596
+ const remaining = new Map(baseline.entries.map((entry) => [keyFor(entry), entry.count]));
11597
+ const diagnostics = {};
11598
+ let baselined = 0;
11599
+ let total = 0;
11600
+ for (const [uri, items] of Object.entries(result.diagnostics)) diagnostics[uri] = [...items].sort((a, b) => a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character).map((diagnostic) => {
11601
+ const key = keyFor(entryFor(root, uri, diagnostic));
11602
+ const count = remaining.get(key) ?? 0;
11603
+ const matched = count > 0;
11604
+ if (matched) {
11605
+ remaining.set(key, count - 1);
11606
+ baselined++;
11607
+ }
11608
+ total++;
11609
+ return {
11610
+ ...diagnostic,
11611
+ baseline: matched
11612
+ };
11613
+ });
11614
+ const stale = [...remaining.values()].reduce((sum, count) => sum + count, 0);
11615
+ const newProblems = total - baselined;
11616
+ return {
11617
+ ...result,
11618
+ diagnostics,
11619
+ passed: newProblems === 0 && (!strict || stale === 0),
11620
+ baselineSummary: {
11621
+ new: newProblems,
11622
+ baselined,
11623
+ total,
11624
+ stale
11625
+ }
11626
+ };
11627
+ }
11628
+ //#endregion
11629
+ //#region src/cli-check.ts
11630
+ /** Executes the check command on target paths with specified options. */
11631
+ async function executeCheckCommand(targetPaths = ["."], opts, setExitCode) {
11632
+ if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
11633
+ const { rootPath, targetPaths: canonicalTargets } = resolveCheckTargets(targetPaths && targetPaths.length > 0 ? targetPaths : ["."]);
11634
+ const format = opts.github ? "github" : opts.format || (process.env.GITHUB_ACTIONS ? "github" : "pretty");
11635
+ if (format === "json") {
11636
+ setProgressMode("off");
11637
+ logger.setDiagnosticStream("stderr");
11638
+ }
11639
+ if (opts.baseline && opts.writeBaseline || opts.baselineStrict && !opts.baseline) throw new ConfigError("Use either --baseline or --write-baseline; --baseline-strict requires --baseline.", "ERR_BASELINE_OPTIONS");
11640
+ const baseline = opts.baseline ? readBaseline(path.resolve(opts.baseline)) : void 0;
11641
+ const checkOptions = {
11642
+ offline: opts.offline,
11643
+ path: rootPath,
11644
+ paths: canonicalTargets,
11645
+ configpath: opts.config,
11646
+ checklevel: opts.checklevel,
11647
+ format,
11648
+ lualsVersion: opts.lualsVersion,
11649
+ failOnError: opts.fail !== false,
11650
+ ignore: opts.ignore,
11651
+ deps: opts.dep
11652
+ };
11653
+ if (opts.config) {
11654
+ const resolvedConfig = path.resolve(opts.config);
11655
+ 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.");
11656
+ }
11657
+ const annotationsPath = await resolveAnnotations({
11658
+ customPath: opts.annotations,
11659
+ offline: opts.offline
11660
+ });
11661
+ const userConfig = loadUserConfig(rootPath, checkOptions.configpath);
11662
+ const realmPlan = planRealmCheck({
11663
+ targetPath: rootPath,
11664
+ userConfig,
11665
+ selection: opts.realm ?? "all",
11666
+ annotationsPath,
11667
+ customConfigPath: checkOptions.configpath,
11668
+ ignore: checkOptions.ignore,
11669
+ targetPaths: canonicalTargets,
11670
+ cliDeps: checkOptions.deps
11671
+ });
11672
+ let result;
11673
+ if (realmPlan) try {
11674
+ result = await runRealmAwareCheck(realmPlan, rootPath, checkOptions);
11675
+ } finally {
11676
+ realmPlan.cleanup();
11677
+ }
11678
+ else {
11679
+ const resolvedDeps = resolvePackageDependencies(rootPath, userConfig, checkOptions.deps);
11680
+ const unrequestedExclusions = computeUnrequestedExclusions(rootPath, canonicalTargets);
11681
+ const resolved = resolveWorkspaceConfig(rootPath, checkOptions.configpath, {
11682
+ ignore: checkOptions.ignore,
11683
+ annotationsPath,
11684
+ dependencyLibraries: resolvedDeps.all,
11685
+ unrequestedExclusions
11686
+ });
11687
+ try {
11688
+ result = await runLuaLSCheck(rootPath, resolved.configPath, checkOptions);
11689
+ } finally {
11690
+ if (resolved.isTemp && fs.existsSync(resolved.configPath)) try {
11691
+ fs.unlinkSync(resolved.configPath);
11692
+ } catch (err) {
11693
+ logger.warn(`Failed to clean up temporary config file ${resolved.configPath}: ${err instanceof Error ? err.message : String(err)}`);
11694
+ }
11695
+ }
11696
+ }
11697
+ if (opts.writeBaseline) {
11698
+ writeBaseline(opts.writeBaseline, result, rootPath);
11699
+ result = compareBaseline(result, createBaseline(result, rootPath), rootPath);
11700
+ } else if (baseline) result = compareBaseline(result, baseline, rootPath, opts.baselineStrict);
11701
+ const output = formatReport(result, checkOptions.format, process.cwd());
11702
+ if (output) writeOutput(output);
11703
+ if (!result.passed && checkOptions.failOnError) setExitCode(1);
11704
+ else setExitCode(0);
11705
+ }
11706
+ //#endregion
11458
11707
  //#region src/cli.ts
11459
11708
  /** Retrieves the formatted package name and version string from package.json. */
11460
11709
  function getVersionString() {
@@ -11466,10 +11715,6 @@ function getVersionString() {
11466
11715
  return "nanos-lint v1.0.0";
11467
11716
  }
11468
11717
  }
11469
- /** Writes command results to stdout unless the level is `silent`. */
11470
- function writeOutput(message) {
11471
- if (logger.isOutputEnabled()) console.log(message);
11472
- }
11473
11718
  /** Accumulates repeatable --ignore pattern arguments, splitting by comma and newline. */
11474
11719
  function collectIgnorePatterns(val, prev) {
11475
11720
  const parts = val.split(/[\r\n,]+/).map((s) => s.trim()).filter(Boolean);
@@ -11485,7 +11730,7 @@ function createProgram(options) {
11485
11730
  "info",
11486
11731
  "debug",
11487
11732
  "silent"
11488
- ]).default(DEFAULT_LOG_LEVEL)).option("--no-progress", "Disable the interactive download, extraction and realm derivation progress display").hook("preAction", (thisCommand, actionCommand) => {
11733
+ ]).default(DEFAULT_LOG_LEVEL)).option("--offline", "Use local dependencies only; never access the network").option("--no-progress", "Disable the interactive download, extraction and realm derivation progress display").hook("preAction", (thisCommand, actionCommand) => {
11489
11734
  const target = actionCommand || thisCommand;
11490
11735
  const opts = target.optsWithGlobals ? target.optsWithGlobals() : target.opts();
11491
11736
  if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
@@ -11503,7 +11748,7 @@ function createProgram(options) {
11503
11748
  "pretty",
11504
11749
  "json",
11505
11750
  "github"
11506
- ])).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([
11751
+ ])).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("--baseline <path>", "Report and fail only on diagnostics absent from a saved baseline").option("--write-baseline <path>", "Record current diagnostics as a baseline and exit successfully").option("--baseline-strict", "Also fail on stale baseline entries (requires --baseline)").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([
11507
11752
  "error",
11508
11753
  "warn",
11509
11754
  "info",
@@ -11514,91 +11759,48 @@ function createProgram(options) {
11514
11759
  "client",
11515
11760
  "server",
11516
11761
  "shared"
11517
- ]).default("all")).action(async (targetPaths = ["."], opts) => {
11518
- if (opts.logLevel && isValidLogLevel(opts.logLevel)) logger.setLevel(opts.logLevel);
11519
- const { rootPath, targetPaths: canonicalTargets } = resolveCheckTargets(targetPaths && targetPaths.length > 0 ? targetPaths : ["."]);
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
- }
11525
- const checkOptions = {
11526
- path: rootPath,
11527
- paths: canonicalTargets,
11528
- configpath: opts.config,
11529
- checklevel: opts.checklevel,
11530
- format,
11531
- lualsVersion: opts.lualsVersion,
11532
- failOnError: opts.fail !== false,
11533
- ignore: opts.ignore,
11534
- deps: opts.dep
11535
- };
11536
- if (opts.config) {
11537
- const resolvedConfig = path.resolve(opts.config);
11538
- 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.");
11539
- }
11540
- const annotationsPath = await resolveAnnotations({ customPath: opts.annotations });
11541
- const userConfig = loadUserConfig(rootPath, checkOptions.configpath);
11542
- const realmPlan = planRealmCheck({
11543
- targetPath: rootPath,
11544
- userConfig,
11545
- selection: opts.realm ?? "all",
11546
- annotationsPath,
11547
- customConfigPath: checkOptions.configpath,
11548
- ignore: checkOptions.ignore,
11549
- targetPaths: canonicalTargets,
11550
- cliDeps: checkOptions.deps
11551
- });
11552
- let result;
11553
- if (realmPlan) try {
11554
- result = await runRealmAwareCheck(realmPlan, rootPath, checkOptions);
11555
- } finally {
11556
- realmPlan.cleanup();
11557
- }
11558
- else {
11559
- const resolvedDeps = resolvePackageDependencies(rootPath, userConfig, checkOptions.deps);
11560
- const unrequestedExclusions = computeUnrequestedExclusions(rootPath, canonicalTargets);
11561
- const resolved = resolveWorkspaceConfig(rootPath, checkOptions.configpath, {
11562
- ignore: checkOptions.ignore,
11563
- annotationsPath,
11564
- dependencyLibraries: resolvedDeps.all,
11565
- unrequestedExclusions
11566
- });
11567
- try {
11568
- result = await runLuaLSCheck(rootPath, resolved.configPath, checkOptions);
11569
- } finally {
11570
- if (resolved.isTemp && fs.existsSync(resolved.configPath)) try {
11571
- fs.unlinkSync(resolved.configPath);
11572
- } catch (err) {
11573
- logger.warn(`Failed to clean up temporary config file ${resolved.configPath}: ${err instanceof Error ? err.message : String(err)}`);
11574
- }
11575
- }
11576
- }
11577
- const output = formatReport(result, checkOptions.format, process.cwd());
11578
- if (output) writeOutput(output);
11579
- if (!result.passed && checkOptions.failOnError) setExitCode(1);
11580
- else setExitCode(0);
11762
+ ]).default("all")).action(async (targetPaths = ["."], opts, cmd) => {
11763
+ opts.offline = cmd.optsWithGlobals().offline;
11764
+ await executeCheckCommand(targetPaths, opts, setExitCode);
11581
11765
  });
11582
- program.command("init [path]").description("Scaffold a .luarc.json configuration in the workspace").option("-f, --force", "Overwrite existing .luarc.json configuration").option("--annotations <path>", "Path to custom annotations.lua file").action(async (targetPath = ".", opts) => {
11583
- const annotationsPath = await resolveAnnotations({ customPath: opts.annotations });
11766
+ program.command("init [path]").description("Scaffold a .luarc.json configuration in the workspace").option("-t, --template <path>", "Custom JSONC .luarc.json scaffold template").option("-f, --force", "Overwrite existing .luarc.json configuration").option("--vendor", "Vendor annotations.lua into .nanos-lint/ inside workspace for portable standalone editor setup").option("--annotations <path>", "Path to custom annotations.lua file (requires --vendor)").action(async (targetPath = ".", opts, cmd) => {
11767
+ opts.offline = cmd.optsWithGlobals().offline;
11768
+ const shouldVendor = Boolean(opts.vendor);
11769
+ const annotationsPath = shouldVendor ? await resolveAnnotations({
11770
+ customPath: opts.annotations,
11771
+ offline: opts.offline
11772
+ }) : opts.annotations;
11584
11773
  writeOutput(`[init] Initialized nanos world LuaLS configuration: ${initWorkspace(path.resolve(targetPath), {
11585
11774
  force: opts.force,
11586
- annotationsPath
11775
+ annotationsPath,
11776
+ vendor: shouldVendor,
11777
+ templatePath: opts.template
11778
+ })}`);
11779
+ setExitCode(0);
11780
+ });
11781
+ program.command("copy-annotations [destination]").alias("export-annotations").description("Copy cached nanos world annotations.lua definitions into a target file or directory (targets not ending in .lua are treated as directories)").option("-f, --force", "Overwrite existing annotations file").option("--annotations <path>", "Path to custom annotations.lua source file").action(async (destination, opts, cmd) => {
11782
+ writeOutput(`[copy-annotations] Copied nanos world annotations to: ${await copyAnnotations(destination, {
11783
+ force: opts.force,
11784
+ annotationsPath: opts.annotations,
11785
+ offline: cmd.optsWithGlobals().offline
11587
11786
  })}`);
11588
11787
  setExitCode(0);
11589
11788
  });
11590
- program.command("warmup").alias("download").description("Pre-fetch and cache both LuaLS binary and annotations for offline execution").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).option("--annotations <path>", "Path to custom annotations.lua file").action(async (opts) => {
11591
- writeOutput(`[warmup] LuaLS binary ready: ${await resolveLuaLSBinary(opts?.lualsVersion || "latest")}`);
11592
- const annotationsPath = await resolveAnnotations({ customPath: opts?.annotations });
11789
+ program.command("warmup").alias("download").description("Pre-fetch and cache both LuaLS binary and annotations for offline execution").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).option("--annotations <path>", "Path to custom annotations.lua file").action(async (opts, cmd) => {
11790
+ writeOutput(`[warmup] LuaLS binary ready: ${await resolveLuaLSBinary(opts?.lualsVersion || "latest", { offline: cmd.optsWithGlobals().offline })}`);
11791
+ const annotationsPath = await resolveAnnotations({
11792
+ customPath: opts?.annotations,
11793
+ offline: cmd.optsWithGlobals().offline
11794
+ });
11593
11795
  const meta = readAnnotationsMetadata();
11594
11796
  writeOutput(`[warmup] nanos world annotations ready: ${annotationsPath}${meta?.commitId && meta.commitId !== "unknown" ? ` (commit ${meta.commitId.slice(0, 7)})` : ""}`);
11595
11797
  writeOutput("[warmup] Cache pre-warmed successfully. Ready for offline execution.");
11596
11798
  setExitCode(0);
11597
11799
  });
11598
- program.command("download-luals [version]").description("Download and cache the LuaLS binary").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).action(async (version, opts) => {
11800
+ program.command("download-luals [version]").description("Download and cache the LuaLS binary").option("--luals-version <ver>", `Version of LuaLS to use (default: ${DEFAULT_LUALS_VERSION})`).action(async (version, opts, cmd) => {
11599
11801
  const ver = version || opts?.lualsVersion || "latest";
11600
11802
  writeOutput(`[luals] Downloading LuaLS ${ver}...`);
11601
- writeOutput(`[luals] Ready at: ${await resolveLuaLSBinary(ver)}`);
11803
+ writeOutput(`[luals] Ready at: ${await resolveLuaLSBinary(ver, { offline: cmd.optsWithGlobals().offline })}`);
11602
11804
  setExitCode(0);
11603
11805
  });
11604
11806
  const handleCleanCache = () => {
@@ -11649,6 +11851,7 @@ Examples:
11649
11851
  $ npx nanos-lint check . --realm server
11650
11852
  $ npx nanos-lint check Shared/ Server/ --realm server
11651
11853
  $ npx nanos-lint init
11854
+ $ npx nanos-lint copy-annotations
11652
11855
  $ npx nanos-lint clean-cache
11653
11856
  `);
11654
11857
  return program;
@@ -11729,6 +11932,6 @@ if (isDirectExecution()) runCLI().then((code) => {
11729
11932
  process.exit(1);
11730
11933
  });
11731
11934
  //#endregion
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 };
11935
+ export { findProjectRoot as $, isProgressInteractive as $t, isAnnotationsValid as A, LOG_LEVELS as An, isGzipArchive as At, shouldEnableColor as B, resolveLuaLSVersion as Bt, MAX_COMMIT_JSON_SIZE_BYTES as C, NanosLintError as Cn, MIN_DOWNLOADED_BINARY_SIZE_BYTES as Ct, getAnnotationsMetadataFilePath as D, systemPaths as Dn, inspectZipMembers as Dt, getAnnotationsCacheDir as E, getDirectorySize as En, getTarBinary as Et, formatProblemSummary as F, parseLogLevel as Fn, getPlatformInfo as Ft, resolvePackageDependencies as G, splitAnnotationsByRealm as Gt, planRealmCheck as H, ALLOWED_DOWNLOAD_DOMAINS as Ht, formatReport as I, DEFAULT_LUALS_VERSION as It, countCheckedFiles as J, SPINNER_FRAMES as Jt, resolveLuaLSBinary as K, PROGRESS_MILESTONES as Kt, formatSeverityBadge as L, FALLBACK_LUALS_VERSION as Lt, updateLastCheckedDate as M, Logger as Mn, parseTarTvSize as Mt, formatGitHubAnnotations as N, isValidLogLevel as Nn, rethrowLuaLSPermissionError as Nt, getCachedAnnotationsFilePath as O, fileUriToPath as On, isBinaryRunnable as Ot, formatPretty as P, logger as Pn, validateArchiveMembers as Pt, findCommonAncestorDirectory as Q, getProgressMode as Qt, getColors as R, fetchLatestLuaLSVersionFromGitHub as Rt, MAX_ANNOTATIONS_SIZE_BYTES as S, LuaLSError as Sn, MAX_DECOMPRESSED_SIZE_BYTES as St, MIN_ANNOTATIONS_SIZE_BYTES as T, formatBytes as Tn, escapePowerShellSingleQuote as Tt, runRealmAwareCheck as U, isAllowedDownloadUrl as Ut, collectRealmFiles as V, sanitizeLuaLSVersion as Vt, collectDeps as W, deriveRealmAnnotationFiles as Wt, computeUnrequestedExclusions as X, createSpinner as Xt, listCheckedFiles as Y, createProgressBar as Yt, filterReportByTargetPaths as Z, formatTransferSummary as Zt, downloadAndCacheAnnotations as _, stripTrailingSlashes as _n, listCachedLuaLSVersionDirs as _t, compareBaseline as a, getDefaultAnnotationsPath as an, MAX_ARCHIVE_SIZE_BYTES as at, getRawAnnotationsUrl as b, CacheError as bn, writeLuaLSMetadata as bt, writeBaseline as c, getPackageRoot as cn, limitDownloadStream as ct, copyAnnotations as d, loadUserConfig as dn, cleanupOldCachedLuaLSVersions as dt, restoreTerminal as en, getCanonicalPath as et, resolveAnnotations as f, mergeConfigs as fn, findExistingLuaLSDir as ft, RAW_ANNOTATIONS_URL as g, stripJsonComments as gn, getLuaLSMetadataPath as gt, GITHUB_COMMITS_API as h, resolveWorkspaceConfig as hn, getIsoWeek as ht, runCLI as i, buildWorkspaceConfig as in, DOWNLOAD_TIMEOUT_MS as it, readAnnotationsMetadata as j, LOG_LEVEL_PRIORITY as jn, isZipArchive as jt, getTodayDateString as k, DEFAULT_LOG_LEVEL as kn, isBinaryValid as kt, formatCacheStatusPretty as l, initWorkspace as ln, trackDownloadProgress as lt, DOCGEN_REPO as m, resolveRealmMappings as mn, getCacheDir as mt, createProgram as n, DEFAULT_REALM_MAPPINGS as nn, matchesTargetPaths as nt, createBaseline as o, getDefaultTemplatePath as on, computeFileSha256 as ot, DOCGEN_BRANCH as p, parseJsonc as pn, getBaseLuaLSCacheDir as pt, runLuaLSCheck as q, PROGRESS_THROTTLE_MS as qt, isDirectExecution as r, VALID_LUALS_DIAGNOSTIC_CODES as rn, resolveCheckTargets as rt, readBaseline as s, getDefinitionsDir as sn, downloadAndExtractLuaLS as st, collectIgnorePatterns as t, setProgressMode as tn, isFilesystemRoot as tt, getCacheStatus as u, loadConfigFile as un, LUALS_METADATA_FILENAME as ut, fetchLatestCommitId as v, writeTempConfig as vn, listCachedLuaLSVersions as vt, METADATA_FILENAME as w, cleanCache as wn, assertValidLuaLSBinary as wt, ANNOTATIONS_FILENAME as x, ConfigError as xn, MAX_ARCHIVE_MEMBER_COUNT as xt, fetchRawAnnotationsContent as y, AnnotationsError as yn, readLuaLSMetadata as yt, pluralize as z, resolveLatestLuaLSVersion as zt };
11733
11936
 
11734
- //# sourceMappingURL=cli-I16SeKRK.js.map
11937
+ //# sourceMappingURL=cli-DN7ZhC9e.js.map