@colrealpro/react-luau-doctor 0.18.0 → 0.18.2

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/dist/cli.js CHANGED
@@ -2,16 +2,16 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import fs10 from "fs";
5
+ import fs11 from "fs";
6
6
  import os2 from "os";
7
- import path12 from "path";
7
+ import path13 from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@colrealpro/react-luau-doctor",
11
11
  publishConfig: {
12
12
  access: "public"
13
13
  },
14
- version: "0.18.0",
14
+ version: "0.18.2",
15
15
  description: "Static analysis for React-Luau hooks, effects, rendering, and performance.",
16
16
  license: "MIT",
17
17
  type: "module",
@@ -9904,7 +9904,6 @@ var GITLAB_WORKFLOW = ".gitlab-ci.yml";
9904
9904
  var SUMMARY_MARKER = "<!-- react-luau-doctor:summary -->";
9905
9905
  var REVIEW_MARKER = "<!-- react-luau-doctor:review -->";
9906
9906
  var MAX_REVIEW_COMMENTS = 20;
9907
- var MAX_RESOLVE_THREADS_PER_REQUEST = 100;
9908
9907
  var MAX_GITHUB_RETRIES = 2;
9909
9908
  var MAX_RATE_LIMIT_WAIT_MS = 60000;
9910
9909
  var PACKAGE_SPEC = `${package_default.name}@${package_default.version}`;
@@ -9920,10 +9919,8 @@ on:
9920
9919
  branches: [main]
9921
9920
 
9922
9921
  permissions:
9923
- # GitHub currently requires Contents: write for the resolveReviewThread GraphQL mutation.
9924
- contents: ${settings.reviewComments ? "write" : "read"}
9922
+ contents: read
9925
9923
  pull-requests: write
9926
- issues: write
9927
9924
  statuses: write
9928
9925
 
9929
9926
  concurrency:
@@ -10332,23 +10329,6 @@ async function githubApi(repo, endpoint, options = {}) {
10332
10329
  const apiBase = (process.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
10333
10330
  return githubRequest(`${apiBase}/repos/${repo}${endpoint}`, options);
10334
10331
  }
10335
- function githubGraphQLUrl() {
10336
- if (process.env.GITHUB_GRAPHQL_URL)
10337
- return process.env.GITHUB_GRAPHQL_URL;
10338
- const apiBase = (process.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
10339
- return apiBase.endsWith("/api/v3") ? `${apiBase.slice(0, -"/v3".length)}/graphql` : `${apiBase}/graphql`;
10340
- }
10341
- async function githubGraphQL(query, variables) {
10342
- const response = await githubRequest(githubGraphQLUrl(), {
10343
- method: "POST",
10344
- body: { query, variables }
10345
- });
10346
- if (response.errors?.length)
10347
- throw new Error(`GitHub GraphQL: ${response.errors.map((error) => error.message ?? "unknown error").join("; ")}`);
10348
- if (!response.data)
10349
- throw new Error("GitHub GraphQL returned no data");
10350
- return response.data;
10351
- }
10352
10332
  function repoRelativeDiagnosticPath(directory, diagnosticFile) {
10353
10333
  const absoluteDirectory = path11.resolve(directory);
10354
10334
  const repoRoot = findGitRoot(absoluteDirectory);
@@ -10458,54 +10438,32 @@ ${diagnostic.message}${diagnostic.help ? `
10458
10438
 
10459
10439
  ${diagnostic.help}` : ""}`;
10460
10440
  }
10461
- async function listReviewThreads(repo, pullNumber) {
10462
- const separator = repo.indexOf("/");
10463
- if (separator < 1 || separator === repo.length - 1)
10464
- throw new Error(`Invalid GitHub repository name: ${repo}`);
10465
- const owner = repo.slice(0, separator);
10466
- const name = repo.slice(separator + 1);
10467
- const threads = [];
10468
- let cursor = null;
10469
- for (;; ) {
10470
- const data = await githubGraphQL(`
10471
- query DoctorReviewThreads($owner: String!, $name: String!, $pull: Int!, $cursor: String) {
10472
- repository(owner: $owner, name: $name) {
10473
- pullRequest(number: $pull) {
10474
- reviewThreads(first: 100, after: $cursor) {
10475
- nodes {
10476
- id
10477
- isResolved
10478
- isOutdated
10479
- path
10480
- comments(first: 1) {
10481
- nodes {
10482
- body
10483
- author { __typename }
10484
- }
10485
- }
10486
- }
10487
- pageInfo { hasNextPage endCursor }
10488
- }
10489
- }
10490
- }
10491
- }
10492
- `, { owner, name, pull: pullNumber, cursor });
10493
- const connection = data.repository?.pullRequest?.reviewThreads;
10494
- if (!connection)
10495
- throw new Error(`Could not load review threads for pull request ${pullNumber}`);
10496
- threads.push(...connection.nodes);
10497
- if (!connection.pageInfo.hasNextPage)
10498
- return threads;
10499
- cursor = connection.pageInfo.endCursor;
10500
- if (!cursor)
10501
- throw new Error("GitHub review thread pagination returned no cursor");
10502
- }
10503
- }
10504
- function doctorThreadFingerprint(thread) {
10505
- const comment = thread.comments.nodes[0];
10506
- if (comment?.author?.__typename !== "Bot" || !comment.body.startsWith(REVIEW_MARKER))
10441
+ var ARCHIVED_REVIEW_BODY = "\u2705 React-Luau Doctor: findings from this review are no longer current. Active findings, if any, are shown in newer reviews.";
10442
+ async function listReviewComments(repo, pullNumber) {
10443
+ const comments = [];
10444
+ for (let page = 1;; page += 1) {
10445
+ const batch = await githubApi(repo, `/pulls/${pullNumber}/comments?per_page=100&page=${page}`);
10446
+ comments.push(...batch);
10447
+ if (batch.length < 100)
10448
+ return comments;
10449
+ }
10450
+ }
10451
+ async function listReviews(repo, pullNumber) {
10452
+ const reviews = [];
10453
+ for (let page = 1;; page += 1) {
10454
+ const batch = await githubApi(repo, `/pulls/${pullNumber}/reviews?per_page=100&page=${page}`);
10455
+ reviews.push(...batch);
10456
+ if (batch.length < 100)
10457
+ return reviews;
10458
+ }
10459
+ }
10460
+ function isDoctorReview(review) {
10461
+ return review.user?.type === "Bot" && (review.body?.includes("React-Luau Doctor") ?? false);
10462
+ }
10463
+ function doctorReviewCommentFingerprint(comment) {
10464
+ if (comment.user?.type !== "Bot" || !comment.body.startsWith(REVIEW_MARKER))
10507
10465
  return null;
10508
- return fingerprintFromReviewBody(comment.body, thread.path);
10466
+ return fingerprintFromReviewBody(comment.body, comment.path);
10509
10467
  }
10510
10468
  function takeCount(counts, key) {
10511
10469
  const count = counts.get(key) ?? 0;
@@ -10514,58 +10472,64 @@ function takeCount(counts, key) {
10514
10472
  counts.set(key, count - 1);
10515
10473
  return true;
10516
10474
  }
10517
- async function resolveReviewThreads(threadIds) {
10518
- for (let offset = 0;offset < threadIds.length; offset += MAX_RESOLVE_THREADS_PER_REQUEST) {
10519
- const batch = threadIds.slice(offset, offset + MAX_RESOLVE_THREADS_PER_REQUEST);
10520
- const declarations = batch.map((_, index) => `$thread${index}: ID!`).join(", ");
10521
- const mutations = batch.map((_, index) => `thread${index}: resolveReviewThread(input: { threadId: $thread${index} }) { thread { id isResolved } }`).join(`
10522
- `);
10523
- const variables = Object.fromEntries(batch.map((id, index) => [`thread${index}`, id]));
10524
- await githubGraphQL(`mutation ResolveDoctorThreads(${declarations}) { ${mutations} }`, variables);
10525
- if (offset + batch.length < threadIds.length)
10526
- await wait(1000);
10527
- }
10475
+ function incrementCount(counts, key) {
10476
+ counts.set(key, (counts.get(key) ?? 0) + 1);
10528
10477
  }
10529
- async function manageReviewComments(repo, pullNumber, currentReport, newDiagnostics, directory, newBase) {
10530
- const threads = await listReviewThreads(repo, pullNumber);
10478
+ async function manageReviewComments(repo, pullNumber, currentReport, newDiagnostics, directory, newBase, pullBase) {
10479
+ const [allReviewComments, reviews] = await Promise.all([
10480
+ listReviewComments(repo, pullNumber),
10481
+ listReviews(repo, pullNumber)
10482
+ ]);
10483
+ const reviewComments = allReviewComments.map((comment) => ({ comment, fingerprint: doctorReviewCommentFingerprint(comment) })).filter((entry) => entry.fingerprint !== null);
10531
10484
  const activeCounts = new Map;
10532
- for (const diagnostic of currentReport.diagnostics) {
10533
- const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10534
- activeCounts.set(fingerprint, (activeCounts.get(fingerprint) ?? 0) + 1);
10535
- }
10485
+ for (const diagnostic of currentReport.diagnostics)
10486
+ incrementCount(activeCounts, diagnosticReviewFingerprint(directory, diagnostic));
10536
10487
  const newCounts = new Map;
10537
- for (const diagnostic of newDiagnostics) {
10538
- const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10539
- newCounts.set(fingerprint, (newCounts.get(fingerprint) ?? 0) + 1);
10540
- }
10541
- const resolvedThreadIds = [];
10542
- for (const thread of threads) {
10543
- if (thread.isResolved)
10544
- continue;
10545
- const fingerprint = doctorThreadFingerprint(thread);
10546
- if (!fingerprint)
10547
- continue;
10548
- if (thread.isOutdated && (newCounts.get(fingerprint) ?? 0) > 0) {
10549
- resolvedThreadIds.push(thread.id);
10488
+ for (const diagnostic of newDiagnostics)
10489
+ incrementCount(newCounts, diagnosticReviewFingerprint(directory, diagnostic));
10490
+ const replacementCounts = new Map;
10491
+ const staleComments = [];
10492
+ const orderedComments = reviewComments.slice().sort((left, right) => Number(left.comment.position === null) - Number(right.comment.position === null));
10493
+ for (const { comment, fingerprint } of orderedComments) {
10494
+ if (comment.position !== null && takeCount(activeCounts, fingerprint)) {
10495
+ takeCount(newCounts, fingerprint);
10550
10496
  continue;
10551
10497
  }
10552
- if (takeCount(activeCounts, fingerprint)) {
10498
+ const needsReplacement = takeCount(activeCounts, fingerprint);
10499
+ if (needsReplacement) {
10500
+ incrementCount(replacementCounts, fingerprint);
10553
10501
  takeCount(newCounts, fingerprint);
10554
- continue;
10555
10502
  }
10556
- resolvedThreadIds.push(thread.id);
10503
+ staleComments.push({ comment, fingerprint, needsReplacement });
10504
+ }
10505
+ const newLineMap = changedLineMap(directory, newBase);
10506
+ const pullLineMap = newBase === pullBase ? newLineMap : changedLineMap(directory, pullBase);
10507
+ const candidates = [];
10508
+ for (const diagnostic of newDiagnostics) {
10509
+ if (!touchesChangedLine(diagnostic, newLineMap.get(diagnostic.file) ?? []))
10510
+ continue;
10511
+ const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10512
+ if (!takeCount(newCounts, fingerprint))
10513
+ continue;
10514
+ candidates.push({ diagnostic, replacement: false });
10557
10515
  }
10558
- const lineMap = changedLineMap(directory, newBase);
10559
- const comments = newDiagnostics.filter((diagnostic) => touchesChangedLine(diagnostic, lineMap.get(diagnostic.file) ?? [])).filter((diagnostic) => {
10516
+ for (const diagnostic of currentReport.diagnostics) {
10517
+ if (!touchesChangedLine(diagnostic, pullLineMap.get(diagnostic.file) ?? []))
10518
+ continue;
10560
10519
  const fingerprint = diagnosticReviewFingerprint(directory, diagnostic);
10561
- return takeCount(newCounts, fingerprint);
10562
- }).slice(0, MAX_REVIEW_COMMENTS).map((diagnostic) => ({
10520
+ if (!takeCount(replacementCounts, fingerprint))
10521
+ continue;
10522
+ candidates.push({ diagnostic, replacement: true });
10523
+ }
10524
+ const selected = candidates.slice(0, MAX_REVIEW_COMMENTS);
10525
+ const comments = selected.map(({ diagnostic }) => ({
10563
10526
  path: repoRelativeDiagnosticPath(directory, diagnostic.file),
10564
10527
  line: diagnostic.location.line,
10565
10528
  side: "RIGHT",
10566
10529
  body: reviewCommentBody(directory, diagnostic)
10567
10530
  }));
10568
10531
  const failures = [];
10532
+ let postedNewComments = true;
10569
10533
  if (comments.length > 0) {
10570
10534
  try {
10571
10535
  await githubApi(repo, `/pulls/${pullNumber}/reviews`, {
@@ -10573,14 +10537,45 @@ async function manageReviewComments(repo, pullNumber, currentReport, newDiagnost
10573
10537
  body: { event: "COMMENT", body: "React-Luau Doctor found new issues", comments }
10574
10538
  });
10575
10539
  } catch (error) {
10540
+ postedNewComments = false;
10576
10541
  failures.push(`could not create new review comments: ${errorMessage(error)}`);
10577
10542
  }
10578
10543
  }
10579
- if (resolvedThreadIds.length > 0) {
10580
- try {
10581
- await resolveReviewThreads(resolvedThreadIds);
10582
- } catch (error) {
10583
- failures.push(`could not resolve fixed review threads: ${errorMessage(error)}`);
10544
+ if (postedNewComments) {
10545
+ const postedReplacementCounts = new Map;
10546
+ for (const candidate of selected) {
10547
+ if (candidate.replacement)
10548
+ incrementCount(postedReplacementCounts, diagnosticReviewFingerprint(directory, candidate.diagnostic));
10549
+ }
10550
+ const remainingByReview = new Map;
10551
+ for (const { comment } of reviewComments) {
10552
+ remainingByReview.set(comment.pull_request_review_id, (remainingByReview.get(comment.pull_request_review_id) ?? 0) + 1);
10553
+ }
10554
+ for (const stale of staleComments) {
10555
+ if (stale.needsReplacement && !takeCount(postedReplacementCounts, stale.fingerprint))
10556
+ continue;
10557
+ try {
10558
+ await githubApi(repo, `/pulls/comments/${stale.comment.id}`, { method: "DELETE" });
10559
+ const reviewId = stale.comment.pull_request_review_id;
10560
+ const remaining = Math.max(0, (remainingByReview.get(reviewId) ?? 1) - 1);
10561
+ remainingByReview.set(reviewId, remaining);
10562
+ } catch (error) {
10563
+ failures.push(`could not delete stale review comment ${stale.comment.id}: ${errorMessage(error)}`);
10564
+ }
10565
+ }
10566
+ for (const review of reviews) {
10567
+ if (!isDoctorReview(review) || review.body === ARCHIVED_REVIEW_BODY)
10568
+ continue;
10569
+ if ((remainingByReview.get(review.id) ?? 0) > 0)
10570
+ continue;
10571
+ try {
10572
+ await githubApi(repo, `/pulls/${pullNumber}/reviews/${review.id}`, {
10573
+ method: "PUT",
10574
+ body: { body: ARCHIVED_REVIEW_BODY }
10575
+ });
10576
+ } catch (error) {
10577
+ failures.push(`could not archive empty review ${review.id}: ${errorMessage(error)}`);
10578
+ }
10584
10579
  }
10585
10580
  }
10586
10581
  if (failures.length > 0)
@@ -10672,7 +10667,7 @@ async function runCiJob(argv) {
10672
10667
  }
10673
10668
  if (settings.reviewComments && base) {
10674
10669
  try {
10675
- await manageReviewComments(repo, pullNumber, report, reviewChanges.diagnostics, path11.resolve(settings.directory), reviewChanges.base ?? base);
10670
+ await manageReviewComments(repo, pullNumber, report, reviewChanges.diagnostics, path11.resolve(settings.directory), reviewChanges.base ?? base, base);
10676
10671
  } catch (error) {
10677
10672
  process.stderr.write(`react-luau-doctor: could not update inline review comments: ${errorMessage(error)}
10678
10673
  `);
@@ -10703,6 +10698,210 @@ async function runCiCommand(argv) {
10703
10698
  throw new Error("ci requires install, config, or upgrade");
10704
10699
  }
10705
10700
 
10701
+ // src/update-check.ts
10702
+ import { spawn } from "child_process";
10703
+ import fs10 from "fs";
10704
+ import path12 from "path";
10705
+ var UPDATE_CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000;
10706
+ var UPDATE_REQUEST_TIMEOUT_MS = 5000;
10707
+ var UPDATE_CACHE_FILENAME = "update-check.json";
10708
+ function updateCacheFilename() {
10709
+ return path12.join(cacheBaseDirectory(), UPDATE_CACHE_FILENAME);
10710
+ }
10711
+ function readUpdateCache() {
10712
+ try {
10713
+ const parsed = JSON.parse(fs10.readFileSync(updateCacheFilename(), "utf8"));
10714
+ if (typeof parsed.checkedAt !== "number" || !Number.isFinite(parsed.checkedAt))
10715
+ return null;
10716
+ if (parsed.latest !== undefined && typeof parsed.latest !== "string")
10717
+ return null;
10718
+ return {
10719
+ checkedAt: parsed.checkedAt,
10720
+ latest: parsed.latest
10721
+ };
10722
+ } catch {
10723
+ return null;
10724
+ }
10725
+ }
10726
+ function writeUpdateCache(cache) {
10727
+ try {
10728
+ const filename = updateCacheFilename();
10729
+ fs10.mkdirSync(path12.dirname(filename), { recursive: true });
10730
+ fs10.writeFileSync(filename, `${JSON.stringify(cache)}
10731
+ `);
10732
+ } catch {}
10733
+ }
10734
+ function parseVersion(value) {
10735
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value.trim());
10736
+ if (!match)
10737
+ return null;
10738
+ return {
10739
+ major: Number(match[1]),
10740
+ minor: Number(match[2]),
10741
+ patch: Number(match[3]),
10742
+ prerelease: match[4]?.split(".") ?? []
10743
+ };
10744
+ }
10745
+ function comparePrerelease(left, right) {
10746
+ if (left.length === 0 || right.length === 0) {
10747
+ if (left.length === right.length)
10748
+ return 0;
10749
+ return left.length === 0 ? 1 : -1;
10750
+ }
10751
+ const count = Math.max(left.length, right.length);
10752
+ for (let index = 0;index < count; index += 1) {
10753
+ const leftPart = left[index];
10754
+ const rightPart = right[index];
10755
+ if (leftPart === undefined)
10756
+ return -1;
10757
+ if (rightPart === undefined)
10758
+ return 1;
10759
+ if (leftPart === rightPart)
10760
+ continue;
10761
+ const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
10762
+ const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
10763
+ if (leftNumber !== null && rightNumber !== null)
10764
+ return leftNumber < rightNumber ? -1 : 1;
10765
+ if (leftNumber !== null)
10766
+ return -1;
10767
+ if (rightNumber !== null)
10768
+ return 1;
10769
+ return leftPart < rightPart ? -1 : 1;
10770
+ }
10771
+ return 0;
10772
+ }
10773
+ function compareVersions(left, right) {
10774
+ const a = parseVersion(left);
10775
+ const b = parseVersion(right);
10776
+ if (!a || !b)
10777
+ return null;
10778
+ for (const key of ["major", "minor", "patch"]) {
10779
+ if (a[key] !== b[key])
10780
+ return a[key] < b[key] ? -1 : 1;
10781
+ }
10782
+ return comparePrerelease(a.prerelease, b.prerelease);
10783
+ }
10784
+ function updateRegistryUrl() {
10785
+ const registry = process.env.REACT_LUAU_DOCTOR_UPDATE_REGISTRY ?? process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? "https://registry.npmjs.org/";
10786
+ const base = registry.endsWith("/") ? registry : `${registry}/`;
10787
+ return new URL(`${encodeURIComponent(package_default.name)}/latest`, base).toString();
10788
+ }
10789
+ async function fetchLatestVersion() {
10790
+ const controller = new AbortController;
10791
+ const timer = setTimeout(() => controller.abort(), UPDATE_REQUEST_TIMEOUT_MS);
10792
+ timer.unref?.();
10793
+ try {
10794
+ const response = await fetch(updateRegistryUrl(), {
10795
+ headers: { accept: "application/json" },
10796
+ signal: controller.signal
10797
+ });
10798
+ if (!response.ok)
10799
+ throw new Error(`npm registry returned HTTP ${response.status}`);
10800
+ const body = await response.json();
10801
+ if (typeof body.version !== "string" || compareVersions(body.version, body.version) === null) {
10802
+ throw new Error("npm registry returned an invalid package version");
10803
+ }
10804
+ return body.version;
10805
+ } finally {
10806
+ clearTimeout(timer);
10807
+ }
10808
+ }
10809
+ function updateCacheIsStale(now = Date.now()) {
10810
+ const cache = readUpdateCache();
10811
+ return !cache || now - cache.checkedAt >= UPDATE_CHECK_INTERVAL_MS;
10812
+ }
10813
+ async function refreshUpdateCache(options = {}) {
10814
+ const now = options.now ?? Date.now();
10815
+ try {
10816
+ const latest = await fetchLatestVersion();
10817
+ writeUpdateCache({ checkedAt: now, latest });
10818
+ return latest;
10819
+ } catch (error) {
10820
+ const previous = readUpdateCache();
10821
+ writeUpdateCache({ checkedAt: now, latest: previous?.latest });
10822
+ if (options.silent)
10823
+ return null;
10824
+ throw error;
10825
+ }
10826
+ }
10827
+ function startBackgroundUpdateRefresh() {
10828
+ if (!updateCacheIsStale())
10829
+ return;
10830
+ const script = process.argv[1];
10831
+ if (!script)
10832
+ return;
10833
+ try {
10834
+ const child2 = spawn(process.execPath, [script, "__update-cache"], {
10835
+ detached: true,
10836
+ stdio: "ignore",
10837
+ windowsHide: true,
10838
+ env: process.env
10839
+ });
10840
+ child2.unref();
10841
+ } catch {}
10842
+ }
10843
+ function getCachedUpdateNotice(currentVersion) {
10844
+ const cache = readUpdateCache();
10845
+ if (!cache?.latest)
10846
+ return null;
10847
+ const comparison = compareVersions(cache.latest, currentVersion);
10848
+ if (comparison === null || comparison <= 0)
10849
+ return null;
10850
+ return { current: currentVersion, latest: cache.latest };
10851
+ }
10852
+ async function checkForUpdatesNow(currentVersion) {
10853
+ const latest = await refreshUpdateCache();
10854
+ if (!latest)
10855
+ throw new Error("Could not check npm for updates");
10856
+ const comparison = compareVersions(latest, currentVersion);
10857
+ if (comparison === null)
10858
+ throw new Error(`Could not compare installed version ${currentVersion} with ${latest}`);
10859
+ return { latest, updateAvailable: comparison > 0 };
10860
+ }
10861
+
10862
+ // src/update-install.ts
10863
+ import { spawnSync as spawnSync3 } from "child_process";
10864
+ import { fileURLToPath } from "url";
10865
+ function normalizedPath(filename) {
10866
+ return filename.replaceAll("\\", "/");
10867
+ }
10868
+ function updateInstallCommandForPath(filename) {
10869
+ const normalized = normalizedPath(filename);
10870
+ const spec = `${package_default.name}@latest`;
10871
+ if (normalized.includes("/.bun/install/global/node_modules/")) {
10872
+ return {
10873
+ manager: "bun",
10874
+ command: "bun",
10875
+ args: ["add", "-g", spec],
10876
+ display: `bun add -g ${spec}`
10877
+ };
10878
+ }
10879
+ if (normalized.includes("/.bun/install/cache/") || normalized.includes("/.npm/_npx/"))
10880
+ return null;
10881
+ if (normalized.includes("/node_modules/")) {
10882
+ return {
10883
+ manager: "npm",
10884
+ command: "npm",
10885
+ args: ["install", "-g", spec],
10886
+ display: `npm install -g ${spec}`
10887
+ };
10888
+ }
10889
+ return null;
10890
+ }
10891
+ function currentUpdateInstallCommand() {
10892
+ return updateInstallCommandForPath(fileURLToPath(import.meta.url));
10893
+ }
10894
+ function installLatestVersion(command) {
10895
+ const result = spawnSync3(command.command, command.args, {
10896
+ stdio: "inherit",
10897
+ windowsHide: true
10898
+ });
10899
+ if (result.error)
10900
+ throw result.error;
10901
+ if (result.status !== 0)
10902
+ throw new Error(`${command.display} exited with code ${result.status ?? "unknown"}`);
10903
+ }
10904
+
10706
10905
  // src/fix-examples.ts
10707
10906
  var examples = {
10708
10907
  "react-luau/parse-error": {
@@ -11189,6 +11388,7 @@ Usage:
11189
11388
  react-luau-doctor [directory] [options]
11190
11389
  react-luau-doctor ci <install|config|upgrade>
11191
11390
  react-luau-doctor why <file:line>
11391
+ react-luau-doctor update [--check]
11192
11392
  react-luau-doctor rules <command>
11193
11393
 
11194
11394
  Scan options:
@@ -11217,6 +11417,7 @@ Scan options:
11217
11417
  --no-color Disable automatic ANSI colors
11218
11418
  --no-cache Disable the persistent OS-level analysis cache
11219
11419
  --no-parallel Disable parallel file analysis
11420
+ --no-update-check Disable the automatic update notice
11220
11421
 
11221
11422
  React-Luau Doctor options:
11222
11423
  --min-severity <level> suggestion, warning, or error
@@ -11229,6 +11430,10 @@ CI commands:
11229
11430
  ci upgrade [--provider github|gitlab] [--pr] [-y] [--cwd <cwd>]
11230
11431
  Reporting toggles: --comment/--no-comment, --review-comments/--no-review-comments, --commit-status/--no-commit-status
11231
11432
 
11433
+ Update commands:
11434
+ update Update the global installation to the latest release
11435
+ update --check Check npm for a newer release without updating
11436
+
11232
11437
  Rules commands:
11233
11438
  rules list [--category <name>] [--configured] [--json]
11234
11439
  rules explain <rule> [--json]
@@ -11241,6 +11446,53 @@ Config:
11241
11446
  react-luau-doctor.config.json
11242
11447
  `;
11243
11448
  }
11449
+ function automaticUpdateNoticeEnabled(options, machineReadable) {
11450
+ return !options.noUpdateCheck && !machineReadable && Boolean(process.stdout.isTTY) && !process.env.CI && process.env.NO_UPDATE_NOTIFIER === undefined && process.env.REACT_LUAU_DOCTOR_NO_UPDATE_CHECK === undefined;
11451
+ }
11452
+ function renderUpdateNotice(current, latest, colorized) {
11453
+ const label = whyPaint(colorized, "Update available:", WHY_ANSI.bold, WHY_ANSI.yellow);
11454
+ const oldVersion = whyPaint(colorized, `v${current}`, WHY_ANSI.dim);
11455
+ const newVersion = whyPaint(colorized, `v${latest}`, WHY_ANSI.bold);
11456
+ return `${label} ${oldVersion} \u2192 ${newVersion}
11457
+ Run \`react-luau-doctor update\` to update.`;
11458
+ }
11459
+ async function runUpdateCommand(argv) {
11460
+ if (argv.includes("--help") || argv.includes("-h")) {
11461
+ process.stdout.write(`Usage: react-luau-doctor update [--check]
11462
+ `);
11463
+ return;
11464
+ }
11465
+ if (argv.length > 1 || argv.length === 1 && argv[0] !== "--check") {
11466
+ throw new Error("Usage: react-luau-doctor update [--check]");
11467
+ }
11468
+ const result = await checkForUpdatesNow(VERSION2);
11469
+ if (!result.updateAvailable) {
11470
+ process.stdout.write(`React-Luau Doctor v${VERSION2} is up to date.
11471
+ `);
11472
+ return;
11473
+ }
11474
+ const colorized = shouldUseColor(false, false);
11475
+ if (argv[0] === "--check") {
11476
+ process.stdout.write(`${renderUpdateNotice(VERSION2, result.latest, colorized)}
11477
+ `);
11478
+ return;
11479
+ }
11480
+ const command = currentUpdateInstallCommand();
11481
+ if (!command) {
11482
+ throw new Error(`Could not determine the global package manager for this installation. Run \`npm install -g ${package_default.name}@latest\` manually.`);
11483
+ }
11484
+ const label = whyPaint(colorized, "Updating React-Luau Doctor:", WHY_ANSI.bold, WHY_ANSI.yellow);
11485
+ const oldVersion = whyPaint(colorized, `v${VERSION2}`, WHY_ANSI.dim);
11486
+ const newVersion = whyPaint(colorized, `v${result.latest}`, WHY_ANSI.bold);
11487
+ process.stdout.write(`${label} ${oldVersion} \u2192 ${newVersion}
11488
+ Using \`${command.display}\`
11489
+
11490
+ `);
11491
+ installLatestVersion(command);
11492
+ process.stdout.write(`
11493
+ Updated React-Luau Doctor to v${result.latest}.
11494
+ `);
11495
+ }
11244
11496
  function splitLongOption(arg) {
11245
11497
  if (!arg.startsWith("--"))
11246
11498
  return { name: arg };
@@ -11646,6 +11898,7 @@ function parseArgs(argv) {
11646
11898
  noColor: false,
11647
11899
  noCache: false,
11648
11900
  noParallel: false,
11901
+ noUpdateCheck: false,
11649
11902
  help: false,
11650
11903
  version: false
11651
11904
  };
@@ -11683,6 +11936,8 @@ function parseArgs(argv) {
11683
11936
  options.noCache = true;
11684
11937
  else if (arg === "--no-parallel")
11685
11938
  options.noParallel = true;
11939
+ else if (arg === "--no-update-check")
11940
+ options.noUpdateCheck = true;
11686
11941
  else if (arg === "--annotations")
11687
11942
  options.annotations = true;
11688
11943
  else if (arg === "--help" || arg === "-h")
@@ -11800,8 +12055,8 @@ function validateModeFlags(options, scope) {
11800
12055
  throw new Error("--annotations cannot be combined with --json or --score");
11801
12056
  }
11802
12057
  function findProjectByName(root, name) {
11803
- const direct = path12.resolve(root, name);
11804
- if (fs10.existsSync(direct) && fs10.statSync(direct).isDirectory())
12058
+ const direct = path13.resolve(root, name);
12059
+ if (fs11.existsSync(direct) && fs11.statSync(direct).isDirectory())
11805
12060
  return direct;
11806
12061
  const ignored = new Set([".git", "node_modules", "Packages", "DevPackages", "ServerPackages", "dist", "vendor"]);
11807
12062
  const queue = [{ directory: root, depth: 0 }];
@@ -11810,17 +12065,17 @@ function findProjectByName(root, name) {
11810
12065
  const current = queue.shift();
11811
12066
  if (current.depth >= 3)
11812
12067
  continue;
11813
- for (const entry of fs10.readdirSync(current.directory, { withFileTypes: true })) {
12068
+ for (const entry of fs11.readdirSync(current.directory, { withFileTypes: true })) {
11814
12069
  if (!entry.isDirectory() || ignored.has(entry.name))
11815
12070
  continue;
11816
- const absolute = path12.join(current.directory, entry.name);
12071
+ const absolute = path13.join(current.directory, entry.name);
11817
12072
  if (entry.name === name)
11818
12073
  matches.push(absolute);
11819
12074
  queue.push({ directory: absolute, depth: current.depth + 1 });
11820
12075
  }
11821
12076
  }
11822
12077
  if (matches.length > 1)
11823
- throw new Error(`Project selector "${name}" is ambiguous: ${matches.map((match) => path12.relative(root, match)).join(", ")}`);
12078
+ throw new Error(`Project selector "${name}" is ambiguous: ${matches.map((match) => path13.relative(root, match)).join(", ")}`);
11824
12079
  return matches[0] ?? null;
11825
12080
  }
11826
12081
  function resolveProjectRoots(root, projectFlag, config) {
@@ -11838,15 +12093,15 @@ function serializeReport(report, compact) {
11838
12093
  return compact ? JSON.stringify(report) : JSON.stringify(report, null, 2);
11839
12094
  }
11840
12095
  function writeJsonFile(filename, value, compact = false) {
11841
- fs10.mkdirSync(path12.dirname(filename), { recursive: true });
11842
- fs10.writeFileSync(filename, `${compact ? JSON.stringify(value) : JSON.stringify(value, null, 2)}
12096
+ fs11.mkdirSync(path13.dirname(filename), { recursive: true });
12097
+ fs11.writeFileSync(filename, `${compact ? JSON.stringify(value) : JSON.stringify(value, null, 2)}
11843
12098
  `);
11844
12099
  }
11845
12100
  function writeDiagnosticsDump(directory, report) {
11846
- fs10.mkdirSync(directory, { recursive: true });
11847
- writeJsonFile(path12.join(directory, "report.json"), report);
11848
- writeJsonFile(path12.join(directory, "diagnostics.json"), report.diagnostics);
11849
- writeJsonFile(path12.join(directory, "summary.json"), {
12101
+ fs11.mkdirSync(directory, { recursive: true });
12102
+ writeJsonFile(path13.join(directory, "report.json"), report);
12103
+ writeJsonFile(path13.join(directory, "diagnostics.json"), report.diagnostics);
12104
+ writeJsonFile(path13.join(directory, "summary.json"), {
11850
12105
  schemaVersion: report.schemaVersion,
11851
12106
  root: report.root,
11852
12107
  scope: report.scope ?? "full",
@@ -11882,7 +12137,7 @@ function parseCommandCwd(argv) {
11882
12137
  const value = argv[++index];
11883
12138
  if (!value)
11884
12139
  throw new Error(`${arg} requires a path`);
11885
- cwd = path12.resolve(value);
12140
+ cwd = path13.resolve(value);
11886
12141
  } else
11887
12142
  remaining.push(arg);
11888
12143
  }
@@ -11991,7 +12246,7 @@ function runRulesCommand(argv) {
11991
12246
  if (!severity)
11992
12247
  throw new Error("Rule severity must be off, suggestion, warning/warn, or error");
11993
12248
  const filename = writeConfig(cwd, (current) => ({ ...current, rules: { ...current.rules, [rule.id]: severity } }));
11994
- process.stdout.write(`Set ${rule.id} to ${severity} in ${path12.relative(cwd, filename)}
12249
+ process.stdout.write(`Set ${rule.id} to ${severity} in ${path13.relative(cwd, filename)}
11995
12250
  `);
11996
12251
  return;
11997
12252
  }
@@ -12010,7 +12265,7 @@ function runRulesCommand(argv) {
12010
12265
  severity = normalized;
12011
12266
  }
12012
12267
  const filename = writeConfig(cwd, (current) => ({ ...current, rules: { ...current.rules, [rule.id]: severity } }));
12013
- process.stdout.write(`Enabled ${rule.id} at ${severity} in ${path12.relative(cwd, filename)}
12268
+ process.stdout.write(`Enabled ${rule.id} at ${severity} in ${path13.relative(cwd, filename)}
12014
12269
  `);
12015
12270
  return;
12016
12271
  }
@@ -12020,7 +12275,7 @@ function runRulesCommand(argv) {
12020
12275
  throw new Error("rules disable requires a rule id");
12021
12276
  const rule = findRule(requested);
12022
12277
  const filename = writeConfig(cwd, (current) => ({ ...current, rules: { ...current.rules, [rule.id]: "off" } }));
12023
- process.stdout.write(`Disabled ${rule.id} in ${path12.relative(cwd, filename)}
12278
+ process.stdout.write(`Disabled ${rule.id} in ${path13.relative(cwd, filename)}
12024
12279
  `);
12025
12280
  return;
12026
12281
  }
@@ -12043,7 +12298,7 @@ function runRulesCommand(argv) {
12043
12298
  ...categoryRules.map((rule) => [rule.id, severity])
12044
12299
  ])
12045
12300
  }));
12046
- process.stdout.write(`Set ${categoryRules.length} ${category} rules to ${severity} in ${path12.relative(cwd, filename)}
12301
+ process.stdout.write(`Set ${categoryRules.length} ${category} rules to ${severity} in ${path13.relative(cwd, filename)}
12047
12302
  `);
12048
12303
  return;
12049
12304
  }
@@ -12127,7 +12382,7 @@ function whyCaretForLine(sourceLine, line, ranges) {
12127
12382
  return value;
12128
12383
  }
12129
12384
  function renderWhyCodeFrame(filename, diagnostic, colorized) {
12130
- const source = fs10.readFileSync(filename, "utf8").split(/\r?\n/);
12385
+ const source = fs11.readFileSync(filename, "utf8").split(/\r?\n/);
12131
12386
  const ranges = whyDiagnosticRanges(diagnostic);
12132
12387
  const intervals = whyFrameIntervals(ranges, source.length);
12133
12388
  const width = String(Math.max(...intervals.map((interval) => interval.end), 1)).length;
@@ -12235,13 +12490,13 @@ async function runWhy(location, cwd, noColor = false, cache = true, onProgress,
12235
12490
  const match = location.match(/^(.*):(\d+)(?::(\d+))?$/);
12236
12491
  if (!match)
12237
12492
  throw new Error("Location must be file:line or file:line:column");
12238
- const filename = path12.resolve(cwd, match[1]);
12493
+ const filename = path13.resolve(cwd, match[1]);
12239
12494
  const line = Number(match[2]);
12240
12495
  const column = match[3] === undefined ? undefined : Number(match[3]);
12241
- if (!fs10.existsSync(filename))
12496
+ if (!fs11.existsSync(filename))
12242
12497
  throw new Error(`File does not exist: ${match[1]}`);
12243
12498
  const colorized = shouldUseColor(noColor, false);
12244
- const source = fs10.readFileSync(filename, "utf8");
12499
+ const source = fs11.readFileSync(filename, "utf8");
12245
12500
  const isSuppressed = createInlineSuppressionChecker(source);
12246
12501
  const auditReport = await scanWhyFile(filename, cwd, cache, onProgress);
12247
12502
  beforeOutput?.();
@@ -12282,16 +12537,16 @@ async function runWhy(location, cwd, noColor = false, cache = true, onProgress,
12282
12537
  }
12283
12538
  }
12284
12539
  function pathIsInside(parent, child2) {
12285
- const relative = path12.relative(parent, child2);
12286
- return relative === "" || !relative.startsWith("..") && !path12.isAbsolute(relative);
12540
+ const relative = path13.relative(parent, child2);
12541
+ return relative === "" || !relative.startsWith("..") && !path13.isAbsolute(relative);
12287
12542
  }
12288
12543
  async function runScan(options, onProgress) {
12289
12544
  const commandRoot = process.cwd();
12290
- const target = path12.resolve(commandRoot, options.target);
12291
- if (!fs10.existsSync(target))
12545
+ const target = path13.resolve(commandRoot, options.target);
12546
+ if (!fs11.existsSync(target))
12292
12547
  throw new Error(`Scan path does not exist: ${options.target}`);
12293
- const targetStat = fs10.statSync(target);
12294
- const scanRoot = targetStat.isFile() ? path12.dirname(target) : target;
12548
+ const targetStat = fs11.statSync(target);
12549
+ const scanRoot = targetStat.isFile() ? path13.dirname(target) : target;
12295
12550
  const loaded = loadConfigWithSource(commandRoot);
12296
12551
  const config = loaded.config;
12297
12552
  const resolvedScope = resolveScope(options, config);
@@ -12321,7 +12576,7 @@ async function runScan(options, onProgress) {
12321
12576
  const reports = [];
12322
12577
  for (const { projectRoot, targetRoot } of projectTargets) {
12323
12578
  let report2;
12324
- const projectName = projectTargets.length > 1 ? path12.relative(displayRoot, projectRoot) || "." : undefined;
12579
+ const projectName = projectTargets.length > 1 ? path13.relative(displayRoot, projectRoot) || "." : undefined;
12325
12580
  const projectProgress = onProgress ? (progress) => onProgress({
12326
12581
  ...progress,
12327
12582
  phase: [projectName, progress.phase].filter(Boolean).join(":") || undefined,
@@ -12368,7 +12623,7 @@ async function runScan(options, onProgress) {
12368
12623
  `[debug] target=${target}`,
12369
12624
  `[debug] config=${loaded.filename ?? "none"}`,
12370
12625
  `[debug] scope=${report.scope ?? scope} base=${report.base ?? resolvedScope.base ?? "auto"}`,
12371
- `[debug] projects=${projectTargets.map(({ projectRoot }) => path12.relative(displayRoot, projectRoot) || ".").join(",")}`,
12626
+ `[debug] projects=${projectTargets.map(({ projectRoot }) => path13.relative(displayRoot, projectRoot) || ".").join(",")}`,
12372
12627
  `[debug] candidates=${report.candidateFiles ?? 0} scanned=${report.scannedFiles} partial=${Boolean(report.partial)}`,
12373
12628
  `[debug] parallel=${!options.noParallel}`
12374
12629
  ];
@@ -12381,6 +12636,14 @@ async function runScan(options, onProgress) {
12381
12636
  async function main() {
12382
12637
  try {
12383
12638
  const argv = process.argv.slice(2);
12639
+ if (argv[0] === "__update-cache") {
12640
+ await refreshUpdateCache({ silent: true });
12641
+ return;
12642
+ }
12643
+ if (argv[0] === "update") {
12644
+ await runUpdateCommand(argv.slice(1));
12645
+ return;
12646
+ }
12384
12647
  if (argv[0] === "ci") {
12385
12648
  await runCiCommand(argv.slice(1));
12386
12649
  return;
@@ -12431,6 +12694,9 @@ ${os2.release()}
12431
12694
  }
12432
12695
  const machineReadable = options.scoreOnly || options.json || options.annotations;
12433
12696
  const colorized = shouldUseColor(options.noColor, machineReadable);
12697
+ const updateNoticeEnabled = automaticUpdateNoticeEnabled(options, machineReadable);
12698
+ if (updateNoticeEnabled)
12699
+ startBackgroundUpdateRefresh();
12434
12700
  const progress = createProgressRenderer({
12435
12701
  enabled: Boolean(process.stdout.isTTY && !process.env.CI && !machineReadable),
12436
12702
  colorized
@@ -12445,12 +12711,12 @@ ${os2.release()}
12445
12711
  const json = `${serializeReport(report, compactJson)}
12446
12712
  `;
12447
12713
  if (options.jsonOut) {
12448
- const outputPath = path12.resolve(process.cwd(), options.jsonOut);
12449
- fs10.mkdirSync(path12.dirname(outputPath), { recursive: true });
12450
- fs10.writeFileSync(outputPath, json);
12714
+ const outputPath = path13.resolve(process.cwd(), options.jsonOut);
12715
+ fs11.mkdirSync(path13.dirname(outputPath), { recursive: true });
12716
+ fs11.writeFileSync(outputPath, json);
12451
12717
  }
12452
12718
  if (options.outputDir)
12453
- writeDiagnosticsDump(path12.resolve(process.cwd(), options.outputDir), report);
12719
+ writeDiagnosticsDump(path13.resolve(process.cwd(), options.outputDir), report);
12454
12720
  const config = loadConfigWithSource(process.cwd()).config;
12455
12721
  const showScore = options.showScore ?? true;
12456
12722
  const verbose = options.verbose ?? config.verbose ?? false;
@@ -12467,6 +12733,13 @@ ${os2.release()}
12467
12733
  } else
12468
12734
  process.stdout.write(`${renderTextReport(report, showScore, colorized, verbose, process.stdout.columns ?? 120)}
12469
12735
  `);
12736
+ if (updateNoticeEnabled) {
12737
+ const update = getCachedUpdateNotice(VERSION2);
12738
+ if (update)
12739
+ process.stdout.write(`
12740
+ ${renderUpdateNotice(update.current, update.latest, colorized)}
12741
+ `);
12742
+ }
12470
12743
  const blocking = options.blocking ?? config.blocking ?? "error";
12471
12744
  if (shouldBlock2(report, blocking))
12472
12745
  process.exitCode = 1;
@@ -12479,5 +12752,5 @@ ${os2.release()}
12479
12752
  }
12480
12753
  main();
12481
12754
 
12482
- //# debugId=77F2455CC417160A64756E2164756E21
12755
+ //# debugId=53584D6CB0EB00E064756E2164756E21
12483
12756
  //# sourceMappingURL=cli.js.map