sickbay 1.7.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Header
3
- } from "./chunk-DRBWOJSY.js";
3
+ } from "./chunk-EONIXEDB.js";
4
4
 
5
5
  // src/components/DiffApp.tsx
6
6
  import React, { useState, useEffect } from "react";
@@ -119,7 +119,7 @@ function DiffApp({ projectPath, branch, jsonOutput, checks, verbose }) {
119
119
  useEffect(() => {
120
120
  (async () => {
121
121
  try {
122
- const { runSickbay } = await import("./dist-GBGEDULY.js");
122
+ const { runSickbay } = await import("./dist-PJQ2AQQL.js");
123
123
  const currentReport = await runSickbay({
124
124
  projectPath,
125
125
  checks,
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  shortName
3
- } from "./chunk-QT7EP2BN.js";
3
+ } from "./chunk-LRBDCBJG.js";
4
4
  import {
5
5
  Header
6
- } from "./chunk-DRBWOJSY.js";
6
+ } from "./chunk-EONIXEDB.js";
7
7
  import {
8
8
  detectPackageManager,
9
9
  detectProject
10
- } from "./chunk-EXMWNJFU.js";
10
+ } from "./chunk-NYE7UH3H.js";
11
11
 
12
12
  // src/components/DoctorApp.tsx
13
13
  import React, { useState, useEffect } from "react";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  shortName
3
- } from "./chunk-QT7EP2BN.js";
3
+ } from "./chunk-LRBDCBJG.js";
4
4
  import {
5
5
  ProgressList
6
6
  } from "./chunk-MBVA75EM.js";
7
7
  import {
8
8
  Header
9
- } from "./chunk-DRBWOJSY.js";
9
+ } from "./chunk-EONIXEDB.js";
10
10
  import {
11
11
  runSickbay
12
- } from "./chunk-EXMWNJFU.js";
12
+ } from "./chunk-NYE7UH3H.js";
13
13
 
14
14
  // src/components/FixApp.tsx
15
15
  import React, { useState, useEffect, useCallback } from "react";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  shortName
3
- } from "./chunk-QT7EP2BN.js";
3
+ } from "./chunk-LRBDCBJG.js";
4
4
  import {
5
5
  Header
6
- } from "./chunk-DRBWOJSY.js";
6
+ } from "./chunk-EONIXEDB.js";
7
7
  import {
8
8
  detectProject
9
- } from "./chunk-EXMWNJFU.js";
9
+ } from "./chunk-NYE7UH3H.js";
10
10
 
11
11
  // src/components/StatsApp.tsx
12
12
  import React, { useState, useEffect } from "react";
@@ -4,11 +4,11 @@ import {
4
4
  } from "./chunk-SHO3ZXTH.js";
5
5
  import {
6
6
  shortName
7
- } from "./chunk-QT7EP2BN.js";
7
+ } from "./chunk-LRBDCBJG.js";
8
8
  import {
9
9
  Header
10
- } from "./chunk-DRBWOJSY.js";
11
- import "./chunk-EXMWNJFU.js";
10
+ } from "./chunk-EONIXEDB.js";
11
+ import "./chunk-NYE7UH3H.js";
12
12
  import {
13
13
  detectRegressions,
14
14
  loadHistory
@@ -14,7 +14,7 @@ import {
14
14
  detectMonorepo,
15
15
  runSickbay,
16
16
  runSickbayMonorepo
17
- } from "./chunk-EXMWNJFU.js";
17
+ } from "./chunk-NYE7UH3H.js";
18
18
  import {
19
19
  loadHistory
20
20
  } from "./chunk-3OR2GFVE.js";
@@ -689,7 +689,7 @@ function TuiApp({
689
689
  return () => timers.forEach(clearTimeout);
690
690
  }, []);
691
691
  useEffect7(() => {
692
- checkForUpdate("1.7.3").then((info) => {
692
+ checkForUpdate("1.7.5").then((info) => {
693
693
  if (info) setUpdateInfo(info);
694
694
  });
695
695
  }, []);
@@ -715,7 +715,7 @@ function TuiApp({
715
715
  } catch {
716
716
  }
717
717
  try {
718
- const { getDependencyTree } = await import("./dist-GBGEDULY.js");
718
+ const { getDependencyTree } = await import("./dist-PJQ2AQQL.js");
719
719
  const { saveDepTree } = await import("./history-XLNVZEDI.js");
720
720
  const tree = await getDependencyTree(projectPath, result.projectInfo.packageManager);
721
721
  saveDepTree(projectPath, tree);
@@ -11,7 +11,7 @@ var ASCII_ART = `
11
11
  A vitals health check for your app
12
12
  `.trim();
13
13
  function Header({ projectName }) {
14
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "green" }, ASCII_ART), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " v", "1.7.3")), projectName && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " Analyzing "), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "white" }, projectName)));
14
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "green" }, ASCII_ART), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " v", "1.7.5")), projectName && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " Analyzing "), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "white" }, projectName)));
15
15
  }
16
16
 
17
17
  export {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  detectMonorepo
3
- } from "./chunk-EXMWNJFU.js";
3
+ } from "./chunk-NYE7UH3H.js";
4
4
 
5
5
  // src/lib/resolve-package.ts
6
6
  import { readFileSync } from "fs";
@@ -87,6 +87,12 @@ function defineConfig(config) {
87
87
  return config;
88
88
  }
89
89
  var CONFIG_FILES = ["sickbay.config.ts", "sickbay.config.js", "sickbay.config.mjs"];
90
+ function getCheckConfig(config, checkId) {
91
+ if (!config?.checks) return null;
92
+ const entry = config.checks[checkId];
93
+ if (typeof entry === "object" && entry.enabled !== false) return entry;
94
+ return null;
95
+ }
90
96
  function isCheckDisabled(config, checkId) {
91
97
  if (!config?.checks) return false;
92
98
  const entry = config.checks[checkId];
@@ -114,6 +120,29 @@ function resolveConfigMeta(config) {
114
120
  const hasCustomConfig = disabledChecks.length > 0 || overriddenChecks.length > 0 || config.weights !== void 0 && Object.keys(config.weights).length > 0 || config.exclude !== void 0 && config.exclude.length > 0;
115
121
  return { hasCustomConfig, overriddenChecks, disabledChecks };
116
122
  }
123
+ var KNOWN_THRESHOLD_KEYS = {
124
+ outdated: ["maxOutdated"],
125
+ complexity: [
126
+ "react-component",
127
+ "custom-hook",
128
+ "node-service",
129
+ "route-file",
130
+ "ts-utility",
131
+ "config",
132
+ "test",
133
+ "general"
134
+ ],
135
+ jscpd: ["warnPercent", "criticalPercent"],
136
+ coverage: ["lines", "functions"],
137
+ eslint: ["maxErrors"],
138
+ typescript: ["maxErrors"],
139
+ madge: ["maxCircular"],
140
+ "todo-scanner": ["patterns"],
141
+ "asset-size": ["imageWarn", "imageCritical", "svgWarn", "fontWarn", "totalWarn", "totalCritical"],
142
+ "source-map-explorer": ["warnSize", "failSize"],
143
+ "license-checker": ["blocklist"],
144
+ git: ["staleMonths", "maxRemoteBranches"]
145
+ };
117
146
  function validateConfig(config, knownCheckIds) {
118
147
  if (config.weights) {
119
148
  for (const [category, value] of Object.entries(config.weights)) {
@@ -123,12 +152,26 @@ function validateConfig(config, knownCheckIds) {
123
152
  }
124
153
  }
125
154
  if (config.checks) {
126
- for (const checkId of Object.keys(config.checks)) {
155
+ for (const [checkId, entry] of Object.entries(config.checks)) {
127
156
  if (!knownCheckIds.includes(checkId)) {
128
157
  process.stderr.write(
129
158
  `Warning: Unknown check "${checkId}" in sickbay.config \u2014 it will be ignored.
130
159
  `
131
160
  );
161
+ continue;
162
+ }
163
+ if (typeof entry === "object" && entry.thresholds) {
164
+ const knownKeys = KNOWN_THRESHOLD_KEYS[checkId];
165
+ if (knownKeys) {
166
+ for (const key of Object.keys(entry.thresholds)) {
167
+ if (!knownKeys.includes(key)) {
168
+ process.stderr.write(
169
+ `Warning: Unknown threshold key "${key}" for check "${checkId}" \u2014 it will be ignored.
170
+ `
171
+ );
172
+ }
173
+ }
174
+ }
132
175
  }
133
176
  }
134
177
  }
@@ -213,6 +256,8 @@ function parseJsonOutput(stdout, fallback = "{}") {
213
256
  return JSON.parse(fallback);
214
257
  }
215
258
  var BaseRunner = class {
259
+ /** Human-readable display name. Falls back to `name` (the check ID) if not set. */
260
+ displayName;
216
261
  applicableFrameworks;
217
262
  applicableRuntimes;
218
263
  isApplicableToContext(context) {
@@ -233,7 +278,7 @@ var BaseRunner = class {
233
278
  return {
234
279
  id: this.name,
235
280
  category: this.category,
236
- name: this.name,
281
+ name: this.displayName ?? this.name,
237
282
  score: 100,
238
283
  status: "skipped",
239
284
  issues: [],
@@ -981,8 +1026,15 @@ var AssetSizeRunner = class extends BaseRunner {
981
1026
  name = "asset-size";
982
1027
  category = "performance";
983
1028
  applicableRuntimes = ["browser"];
984
- async run(projectPath) {
1029
+ async run(projectPath, options) {
985
1030
  const elapsed = timer();
1031
+ const t = options?.checkConfig?.thresholds;
1032
+ const imageWarn = t?.imageWarn ?? IMAGE_WARN;
1033
+ const imageCritical = t?.imageCritical ?? IMAGE_CRITICAL;
1034
+ const svgWarn = t?.svgWarn ?? SVG_WARN;
1035
+ const fontWarn = t?.fontWarn ?? FONT_WARN;
1036
+ const totalWarn = t?.totalWarn ?? TOTAL_WARN;
1037
+ const totalCritical = t?.totalCritical ?? TOTAL_CRITICAL;
986
1038
  try {
987
1039
  const assets = [];
988
1040
  for (const dir of ASSET_DIRS) {
@@ -997,7 +1049,7 @@ var AssetSizeRunner = class extends BaseRunner {
997
1049
  const sizeKB = Math.round(asset.size / 1024);
998
1050
  const sizeMB = (asset.size / (1024 * 1024)).toFixed(1);
999
1051
  if (asset.type === "image") {
1000
- if (asset.size > IMAGE_CRITICAL) {
1052
+ if (asset.size > imageCritical) {
1001
1053
  issues.push({
1002
1054
  severity: "critical",
1003
1055
  message: `${asset.path} \u2014 ${sizeMB}MB image (exceeds 2MB)`,
@@ -1007,7 +1059,7 @@ var AssetSizeRunner = class extends BaseRunner {
1007
1059
  },
1008
1060
  reportedBy: ["asset-size"]
1009
1061
  });
1010
- } else if (asset.size > IMAGE_WARN) {
1062
+ } else if (asset.size > imageWarn) {
1011
1063
  issues.push({
1012
1064
  severity: "warning",
1013
1065
  message: `${asset.path} \u2014 ${sizeKB}KB image (exceeds 500KB)`,
@@ -1017,7 +1069,7 @@ var AssetSizeRunner = class extends BaseRunner {
1017
1069
  });
1018
1070
  }
1019
1071
  } else if (asset.type === "svg") {
1020
- if (asset.size > SVG_WARN) {
1072
+ if (asset.size > svgWarn) {
1021
1073
  issues.push({
1022
1074
  severity: "warning",
1023
1075
  message: `${asset.path} \u2014 ${sizeKB}KB SVG (exceeds 100KB, likely unoptimized)`,
@@ -1029,7 +1081,7 @@ var AssetSizeRunner = class extends BaseRunner {
1029
1081
  });
1030
1082
  }
1031
1083
  } else if (asset.type === "font") {
1032
- if (asset.size > FONT_WARN) {
1084
+ if (asset.size > fontWarn) {
1033
1085
  issues.push({
1034
1086
  severity: "warning",
1035
1087
  message: `${asset.path} \u2014 ${sizeKB}KB font (exceeds 500KB)`,
@@ -1043,14 +1095,14 @@ var AssetSizeRunner = class extends BaseRunner {
1043
1095
  }
1044
1096
  }
1045
1097
  const totalMB = (totalSize / (1024 * 1024)).toFixed(1);
1046
- if (totalSize > TOTAL_CRITICAL) {
1098
+ if (totalSize > totalCritical) {
1047
1099
  issues.push({
1048
1100
  severity: "critical",
1049
1101
  message: `Total asset size is ${totalMB}MB \u2014 exceeds 10MB threshold`,
1050
1102
  fix: { description: "Review and optimize all static assets to reduce total payload" },
1051
1103
  reportedBy: ["asset-size"]
1052
1104
  });
1053
- } else if (totalSize > TOTAL_WARN) {
1105
+ } else if (totalSize > totalWarn) {
1054
1106
  issues.push({
1055
1107
  severity: "warning",
1056
1108
  message: `Total asset size is ${totalMB}MB \u2014 consider optimizing`,
@@ -1201,8 +1253,25 @@ var ComplexityRunner = class extends BaseRunner {
1201
1253
  async isApplicable(projectPath) {
1202
1254
  return SOURCE_DIRS.some((dir) => existsSync5(join11(projectPath, dir)));
1203
1255
  }
1204
- async run(projectPath) {
1256
+ async run(projectPath, options) {
1205
1257
  const elapsed = timer();
1258
+ const userThresholds = options?.checkConfig?.thresholds;
1259
+ const mergedThresholds = { ...FILE_TYPE_THRESHOLDS };
1260
+ if (userThresholds) {
1261
+ for (const [key, overrides] of Object.entries(userThresholds)) {
1262
+ const ft = key;
1263
+ if (mergedThresholds[ft] && overrides) {
1264
+ mergedThresholds[ft] = {
1265
+ warn: overrides.warn ?? mergedThresholds[ft].warn,
1266
+ critical: overrides.critical ?? mergedThresholds[ft].critical
1267
+ };
1268
+ }
1269
+ }
1270
+ }
1271
+ const resolveThresholds = (filePath) => {
1272
+ const fileType = classifyFile(filePath);
1273
+ return { ...mergedThresholds[fileType], fileType };
1274
+ };
1206
1275
  try {
1207
1276
  const files = SOURCE_DIRS.flatMap(
1208
1277
  (dir) => existsSync5(join11(projectPath, dir)) ? scanDirectory(join11(projectPath, dir), projectPath) : []
@@ -1210,7 +1279,7 @@ var ComplexityRunner = class extends BaseRunner {
1210
1279
  const issues = [];
1211
1280
  let oversizedCount = 0;
1212
1281
  for (const f of files) {
1213
- const { warn, critical, fileType } = getThresholds(f.path);
1282
+ const { warn, critical, fileType } = resolveThresholds(f.path);
1214
1283
  if (f.lines >= warn) {
1215
1284
  oversizedCount++;
1216
1285
  const label = getFileTypeLabel(fileType);
@@ -1226,7 +1295,7 @@ var ComplexityRunner = class extends BaseRunner {
1226
1295
  const avgLines = files.length > 0 ? Math.round(totalLines / files.length) : 0;
1227
1296
  const score = Math.max(0, 100 - oversizedCount * 10);
1228
1297
  const topFiles = [...files].sort((a, b) => b.lines - a.lines).slice(0, 10).map((f) => {
1229
- const { warn, critical, fileType } = getThresholds(f.path);
1298
+ const { warn, critical, fileType } = resolveThresholds(f.path);
1230
1299
  return { ...f, fileType, warn, critical };
1231
1300
  });
1232
1301
  return {
@@ -1454,11 +1523,14 @@ var CoverageRunner = class extends BaseRunner {
1454
1523
  }
1455
1524
  return true;
1456
1525
  }
1457
- async run(projectPath) {
1526
+ async run(projectPath, options) {
1458
1527
  const elapsed = timer();
1528
+ const thresholds = options?.checkConfig?.thresholds;
1529
+ const lineTarget = thresholds?.lines ?? 80;
1530
+ const functionTarget = thresholds?.functions ?? 80;
1459
1531
  const runner = this.detectTestRunner(projectPath);
1460
1532
  if (!runner) {
1461
- return this.readExistingCoverage(projectPath, elapsed);
1533
+ return this.readExistingCoverage(projectPath, elapsed, lineTarget);
1462
1534
  }
1463
1535
  try {
1464
1536
  const hasCoverage = this.hasCoverageProvider(projectPath, runner);
@@ -1513,7 +1585,9 @@ var CoverageRunner = class extends BaseRunner {
1513
1585
  coverageData,
1514
1586
  runner,
1515
1587
  hasCoverage,
1516
- packageManager
1588
+ packageManager,
1589
+ lineTarget,
1590
+ functionTarget
1517
1591
  );
1518
1592
  } catch (err) {
1519
1593
  return {
@@ -1530,7 +1604,7 @@ var CoverageRunner = class extends BaseRunner {
1530
1604
  };
1531
1605
  }
1532
1606
  }
1533
- buildResult(elapsed, counts, coverage, runner, hasCoverage, packageManager = "npm") {
1607
+ buildResult(elapsed, counts, coverage, runner, hasCoverage, packageManager = "npm", lineTarget = 80, functionTarget = 80) {
1534
1608
  const issues = [];
1535
1609
  if (counts.failed > 0) {
1536
1610
  issues.push({
@@ -1541,18 +1615,18 @@ var CoverageRunner = class extends BaseRunner {
1541
1615
  });
1542
1616
  }
1543
1617
  if (coverage) {
1544
- if (coverage.lines.pct < 80) {
1618
+ if (coverage.lines.pct < lineTarget) {
1545
1619
  issues.push({
1546
1620
  severity: coverage.lines.pct < 50 ? "critical" : "warning",
1547
- message: `Line coverage: ${coverage.lines.pct.toFixed(1)}% (target: 80%)`,
1621
+ message: `Line coverage: ${coverage.lines.pct.toFixed(1)}% (target: ${lineTarget}%)`,
1548
1622
  fix: { description: "Add tests to improve coverage" },
1549
1623
  reportedBy: ["coverage"]
1550
1624
  });
1551
1625
  }
1552
- if (coverage.functions.pct < 80) {
1626
+ if (coverage.functions.pct < functionTarget) {
1553
1627
  issues.push({
1554
1628
  severity: "warning",
1555
- message: `Function coverage: ${coverage.functions.pct.toFixed(1)}% (target: 80%)`,
1629
+ message: `Function coverage: ${coverage.functions.pct.toFixed(1)}% (target: ${functionTarget}%)`,
1556
1630
  fix: { description: "Add tests for uncovered functions" },
1557
1631
  reportedBy: ["coverage"]
1558
1632
  });
@@ -1576,7 +1650,7 @@ var CoverageRunner = class extends BaseRunner {
1576
1650
  const covAvg = (coverage.lines.pct + coverage.statements.pct + coverage.functions.pct + coverage.branches.pct) / 4;
1577
1651
  score = Math.round(score * 0.6 + covAvg * 0.4);
1578
1652
  }
1579
- const status = counts.failed > 0 ? "fail" : coverage && coverage.lines.pct < 50 ? "fail" : coverage && coverage.lines.pct < 80 ? "warning" : issues.length > 0 ? "warning" : "pass";
1653
+ const status = counts.failed > 0 ? "fail" : coverage && coverage.lines.pct < 50 ? "fail" : coverage && coverage.lines.pct < lineTarget ? "warning" : issues.length > 0 ? "warning" : "pass";
1580
1654
  return {
1581
1655
  id: "coverage",
1582
1656
  category: this.category,
@@ -1601,7 +1675,7 @@ var CoverageRunner = class extends BaseRunner {
1601
1675
  }
1602
1676
  };
1603
1677
  }
1604
- readExistingCoverage(projectPath, elapsed) {
1678
+ readExistingCoverage(projectPath, elapsed, lineTarget = 80) {
1605
1679
  const coveragePath = COVERAGE_PATHS.map((p) => join13(projectPath, p)).find(existsSync7);
1606
1680
  if (!coveragePath) {
1607
1681
  return this.skipped("No test runner or coverage report found");
@@ -1615,10 +1689,10 @@ var CoverageRunner = class extends BaseRunner {
1615
1689
  const { lines, statements, functions, branches } = candidate;
1616
1690
  const avg = (lines.pct + statements.pct + functions.pct + branches.pct) / 4;
1617
1691
  const issues = [];
1618
- if (lines.pct < 80) {
1692
+ if (lines.pct < lineTarget) {
1619
1693
  issues.push({
1620
1694
  severity: lines.pct < 50 ? "critical" : "warning",
1621
- message: `Line coverage: ${lines.pct.toFixed(1)}% (target: 80%)`,
1695
+ message: `Line coverage: ${lines.pct.toFixed(1)}% (target: ${lineTarget}%)`,
1622
1696
  reportedBy: ["coverage"]
1623
1697
  });
1624
1698
  }
@@ -1724,8 +1798,10 @@ var ESLintRunner = class extends BaseRunner {
1724
1798
  async isApplicable(projectPath) {
1725
1799
  return existsSync8(join14(projectPath, ".eslintrc.js")) || existsSync8(join14(projectPath, ".eslintrc.cjs")) || existsSync8(join14(projectPath, ".eslintrc.json")) || existsSync8(join14(projectPath, ".eslintrc.yml")) || existsSync8(join14(projectPath, "eslint.config.js")) || existsSync8(join14(projectPath, "eslint.config.mjs")) || existsSync8(join14(projectPath, "eslint.config.cjs"));
1726
1800
  }
1727
- async run(projectPath) {
1801
+ async run(projectPath, options) {
1728
1802
  const elapsed = timer();
1803
+ const thresholds = options?.checkConfig?.thresholds;
1804
+ const maxErrors = thresholds?.maxErrors ?? 10;
1729
1805
  try {
1730
1806
  const candidateDirs = ["src", "lib", "app"];
1731
1807
  const dirsToScan = candidateDirs.filter((d) => existsSync8(join14(projectPath, d)));
@@ -1769,7 +1845,7 @@ var ESLintRunner = class extends BaseRunner {
1769
1845
  category: this.category,
1770
1846
  name: "Lint",
1771
1847
  score,
1772
- status: totalErrors > 10 ? "fail" : totalErrors > 0 ? "warning" : totalWarnings > 0 ? "warning" : "pass",
1848
+ status: totalErrors > maxErrors ? "fail" : totalErrors > 0 ? "warning" : totalWarnings > 0 ? "warning" : "pass",
1773
1849
  issues,
1774
1850
  toolsUsed: ["eslint"],
1775
1851
  duration: elapsed(),
@@ -1797,8 +1873,11 @@ var GitRunner = class extends BaseRunner {
1797
1873
  async isApplicable(projectPath) {
1798
1874
  return fileExists(projectPath, ".git");
1799
1875
  }
1800
- async run(projectPath) {
1876
+ async run(projectPath, options) {
1801
1877
  const elapsed = timer();
1878
+ const thresholds = options?.checkConfig?.thresholds;
1879
+ const staleMonths = thresholds?.staleMonths ?? 6;
1880
+ const maxRemoteBranches = thresholds?.maxRemoteBranches ?? 20;
1802
1881
  try {
1803
1882
  const [lastCommitResult, logCountResult, contributorsResult] = await Promise.allSettled([
1804
1883
  execa5("git", ["log", "-1", "--format=%cr"], { cwd: projectPath }),
@@ -1819,7 +1898,7 @@ var GitRunner = class extends BaseRunner {
1819
1898
  const commitCount = logCountResult.status === "fulfilled" ? parseInt(logCountResult.value.stdout.trim(), 10) : 0;
1820
1899
  const contributorCount = contributorsResult.status === "fulfilled" ? contributorsResult.value.stdout.trim().split("\n").filter(Boolean).length : 0;
1821
1900
  const issues = [];
1822
- const isStale = lastCommit.includes("year") || lastCommit.includes("month") && parseInt(lastCommit) > 6;
1901
+ const isStale = lastCommit.includes("year") || lastCommit.includes("month") && parseInt(lastCommit) > staleMonths;
1823
1902
  if (isStale) {
1824
1903
  issues.push({
1825
1904
  severity: "warning",
@@ -1827,7 +1906,7 @@ var GitRunner = class extends BaseRunner {
1827
1906
  reportedBy: ["git"]
1828
1907
  });
1829
1908
  }
1830
- if (remoteBranches > 20) {
1909
+ if (remoteBranches > maxRemoteBranches) {
1831
1910
  issues.push({
1832
1911
  severity: "info",
1833
1912
  message: `${remoteBranches} remote branches \u2014 consider pruning stale branches`,
@@ -2025,8 +2104,11 @@ var HeavyDepsRunner = class extends BaseRunner {
2025
2104
  var JscpdRunner = class extends BaseRunner {
2026
2105
  name = "jscpd";
2027
2106
  category = "code-quality";
2028
- async run(projectPath) {
2107
+ async run(projectPath, options) {
2029
2108
  const elapsed = timer();
2109
+ const thresholds = options?.checkConfig?.thresholds;
2110
+ const warnPercent = thresholds?.warnPercent ?? 5;
2111
+ const criticalPercent = thresholds?.criticalPercent ?? 20;
2030
2112
  const available = await isCommandAvailable("jscpd");
2031
2113
  if (!available) {
2032
2114
  return this.skipped("jscpd not installed \u2014 run: npm i -g jscpd");
@@ -2050,9 +2132,9 @@ var JscpdRunner = class extends BaseRunner {
2050
2132
  const percentage = data.statistics?.total.percentage ?? 0;
2051
2133
  const clones = data.statistics?.total.clones ?? 0;
2052
2134
  const issues = [];
2053
- if (percentage > 5) {
2135
+ if (percentage > warnPercent) {
2054
2136
  issues.push({
2055
- severity: percentage > 20 ? "critical" : "warning",
2137
+ severity: percentage > criticalPercent ? "critical" : "warning",
2056
2138
  message: `${percentage.toFixed(1)}% code duplication detected (${clones} clone${clones !== 1 ? "s" : ""})`,
2057
2139
  fix: {
2058
2140
  description: "Extract duplicated code into shared utilities or components"
@@ -2065,7 +2147,7 @@ var JscpdRunner = class extends BaseRunner {
2065
2147
  category: this.category,
2066
2148
  name: "Code Duplication",
2067
2149
  score: Math.max(0, 100 - Math.round(percentage * 3)),
2068
- status: percentage === 0 ? "pass" : percentage > 20 ? "fail" : percentage > 5 ? "warning" : "pass",
2150
+ status: percentage === 0 ? "pass" : percentage > criticalPercent ? "fail" : percentage > warnPercent ? "warning" : "pass",
2069
2151
  issues,
2070
2152
  toolsUsed: ["jscpd"],
2071
2153
  duration: elapsed(),
@@ -2217,8 +2299,10 @@ var PROBLEMATIC_LICENSES = ["GPL-2.0", "GPL-3.0", "AGPL-3.0", "LGPL-2.1", "LGPL-
2217
2299
  var LicenseCheckerRunner = class extends BaseRunner {
2218
2300
  name = "license-checker";
2219
2301
  category = "security";
2220
- async run(projectPath) {
2302
+ async run(projectPath, options) {
2221
2303
  const elapsed = timer();
2304
+ const thresholds = options?.checkConfig?.thresholds;
2305
+ const blocklist = thresholds?.blocklist ?? PROBLEMATIC_LICENSES;
2222
2306
  const available = await isCommandAvailable("license-checker");
2223
2307
  if (!available) {
2224
2308
  return this.skipped("license-checker not installed \u2014 run: npm i -g license-checker");
@@ -2234,7 +2318,7 @@ var LicenseCheckerRunner = class extends BaseRunner {
2234
2318
  const issues = [];
2235
2319
  for (const [pkg, info] of Object.entries(licenses)) {
2236
2320
  const license = info.licenses;
2237
- if (PROBLEMATIC_LICENSES.some((l) => license.includes(l))) {
2321
+ if (blocklist.some((l) => license.includes(l))) {
2238
2322
  issues.push({
2239
2323
  severity: "warning",
2240
2324
  message: `${pkg} uses ${license} license \u2014 may be incompatible with commercial use`,
@@ -2309,8 +2393,10 @@ function findCircularDeps(graph) {
2309
2393
  var MadgeRunner = class extends BaseRunner {
2310
2394
  name = "madge";
2311
2395
  category = "code-quality";
2312
- async run(projectPath) {
2396
+ async run(projectPath, options) {
2313
2397
  const elapsed = timer();
2398
+ const thresholds = options?.checkConfig?.thresholds;
2399
+ const maxCircular = thresholds?.maxCircular ?? 5;
2314
2400
  const available = await isCommandAvailable("madge");
2315
2401
  if (!available) {
2316
2402
  return this.skipped("madge not installed \u2014 run: npm i -g madge");
@@ -2345,7 +2431,7 @@ var MadgeRunner = class extends BaseRunner {
2345
2431
  category: this.category,
2346
2432
  name: "Circular Dependencies",
2347
2433
  score: circles.length === 0 ? 100 : Math.max(0, 100 - circles.length * 10),
2348
- status: circles.length === 0 ? "pass" : circles.length > 5 ? "fail" : "warning",
2434
+ status: circles.length === 0 ? "pass" : circles.length > maxCircular ? "fail" : "warning",
2349
2435
  issues,
2350
2436
  toolsUsed: ["madge"],
2351
2437
  duration: elapsed(),
@@ -3298,9 +3384,11 @@ var NpmAuditRunner = class extends BaseRunner {
3298
3384
  var OutdatedRunner = class extends BaseRunner {
3299
3385
  name = "outdated";
3300
3386
  category = "dependencies";
3301
- async run(projectPath) {
3387
+ async run(projectPath, options) {
3302
3388
  const elapsed = timer();
3303
3389
  const pm = detectPackageManager(projectPath);
3390
+ const thresholds = options?.checkConfig?.thresholds;
3391
+ const maxOutdated = thresholds?.maxOutdated ?? 15;
3304
3392
  if (pm === "yarn" || pm === "bun") {
3305
3393
  return this.skipped(`${pm} outdated not supported \u2014 run: ${pm} outdated`);
3306
3394
  }
@@ -3333,7 +3421,7 @@ var OutdatedRunner = class extends BaseRunner {
3333
3421
  category: this.category,
3334
3422
  name: "Outdated Packages",
3335
3423
  score: Math.max(0, 100 - count * 3),
3336
- status: count === 0 ? "pass" : count > 15 ? "fail" : "warning",
3424
+ status: count === 0 ? "pass" : count > maxOutdated ? "fail" : "warning",
3337
3425
  issues,
3338
3426
  toolsUsed: [pm],
3339
3427
  duration: elapsed(),
@@ -3740,10 +3828,14 @@ function findEntryChunks(buildPath) {
3740
3828
  }
3741
3829
  var SourceMapExplorerRunner = class extends BaseRunner {
3742
3830
  name = "source-map-explorer";
3831
+ displayName = "Bundle Size";
3743
3832
  category = "performance";
3744
3833
  applicableRuntimes = ["browser"];
3745
- async run(projectPath) {
3834
+ async run(projectPath, options) {
3746
3835
  const elapsed = timer();
3836
+ const t = options?.checkConfig?.thresholds;
3837
+ const warnSize = t?.warnSize ?? SIZE_THRESHOLD_WARN;
3838
+ const failSize = t?.failSize ?? SIZE_THRESHOLD_FAIL;
3747
3839
  const buildDir = fileExists(projectPath, "dist") ? "dist" : "build";
3748
3840
  const buildPath = join29(projectPath, buildDir);
3749
3841
  const mapFiles = await globby("**/*.js.map", {
@@ -3770,7 +3862,7 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3770
3862
  const totalKB = Math.round(totalBytes / 1024);
3771
3863
  const largestKB = Math.round(largestBytes / 1024);
3772
3864
  const issues = [];
3773
- if (largestBytes > SIZE_THRESHOLD_FAIL) {
3865
+ if (largestBytes > failSize) {
3774
3866
  issues.push({
3775
3867
  severity: "critical",
3776
3868
  message: `Largest bundle is ${largestKB}KB \u2014 exceeds 1MB threshold`,
@@ -3779,7 +3871,7 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3779
3871
  },
3780
3872
  reportedBy: ["source-map-explorer"]
3781
3873
  });
3782
- } else if (largestBytes > SIZE_THRESHOLD_WARN) {
3874
+ } else if (largestBytes > warnSize) {
3783
3875
  issues.push({
3784
3876
  severity: "warning",
3785
3877
  message: `Largest bundle is ${largestKB}KB \u2014 consider optimizing`,
@@ -3789,7 +3881,7 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3789
3881
  reportedBy: ["source-map-explorer"]
3790
3882
  });
3791
3883
  }
3792
- const score = largestBytes > SIZE_THRESHOLD_FAIL ? 40 : largestBytes > SIZE_THRESHOLD_WARN ? 70 : 100;
3884
+ const score = largestBytes > failSize ? 40 : largestBytes > warnSize ? 70 : 100;
3793
3885
  return {
3794
3886
  id: "source-map-explorer",
3795
3887
  category: this.category,
@@ -3842,7 +3934,7 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3842
3934
  const totalKB = Math.round(totalBytes / 1024);
3843
3935
  const initialKB = Math.round(initialBytes / 1024);
3844
3936
  const issues = [];
3845
- if (initialBytes > SIZE_THRESHOLD_FAIL) {
3937
+ if (initialBytes > failSize) {
3846
3938
  issues.push({
3847
3939
  severity: "critical",
3848
3940
  message: `Initial bundle is ${initialKB}KB \u2014 exceeds 1MB threshold`,
@@ -3851,7 +3943,7 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3851
3943
  },
3852
3944
  reportedBy: ["bundle-size-check"]
3853
3945
  });
3854
- } else if (initialBytes > SIZE_THRESHOLD_WARN) {
3946
+ } else if (initialBytes > warnSize) {
3855
3947
  issues.push({
3856
3948
  severity: "warning",
3857
3949
  message: `Initial bundle is ${initialKB}KB \u2014 consider optimizing`,
@@ -3861,7 +3953,7 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3861
3953
  reportedBy: ["bundle-size-check"]
3862
3954
  });
3863
3955
  }
3864
- const score = initialBytes > SIZE_THRESHOLD_FAIL ? 40 : initialBytes > SIZE_THRESHOLD_WARN ? 70 : 100;
3956
+ const score = initialBytes > failSize ? 40 : initialBytes > warnSize ? 70 : 100;
3865
3957
  return {
3866
3958
  id: "source-map-explorer",
3867
3959
  category: this.category,
@@ -3902,7 +3994,10 @@ var SourceMapExplorerRunner = class extends BaseRunner {
3902
3994
  }
3903
3995
  }
3904
3996
  };
3905
- var TODO_PATTERN = /\b(TODO|FIXME|HACK)\b[:\s]*(.*)/i;
3997
+ var DEFAULT_PATTERNS = ["TODO", "FIXME", "HACK"];
3998
+ function buildPattern(patterns) {
3999
+ return new RegExp("\\b(" + patterns.join("|") + ")\\b[:\\s]*(.*)", "i");
4000
+ }
3906
4001
  var STRING_LITERAL_RE = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g;
3907
4002
  function stripStringLiterals(line) {
3908
4003
  return line.replace(STRING_LITERAL_RE, (m) => " ".repeat(m.length));
@@ -3914,10 +4009,13 @@ var TodoScannerRunner = class extends BaseRunner {
3914
4009
  async isApplicable(projectPath) {
3915
4010
  return existsSync18(join30(projectPath, "src"));
3916
4011
  }
3917
- async run(projectPath) {
4012
+ async run(projectPath, options) {
3918
4013
  const elapsed = timer();
4014
+ const thresholds = options?.checkConfig?.thresholds;
4015
+ const patterns = thresholds?.patterns ?? DEFAULT_PATTERNS;
4016
+ const pattern = buildPattern(patterns);
3919
4017
  try {
3920
- const todos = scanDirectory4(join30(projectPath, "src"), projectPath);
4018
+ const todos = scanDirectory4(join30(projectPath, "src"), projectPath, pattern);
3921
4019
  const issues = todos.map((t) => ({
3922
4020
  severity: t.kind === "FIXME" || t.kind === "HACK" ? "warning" : "info",
3923
4021
  message: `${t.file}:${t.line} \u2014 ${t.kind}: ${t.text || "(no description)"}`,
@@ -3962,7 +4060,7 @@ var TodoScannerRunner = class extends BaseRunner {
3962
4060
  }
3963
4061
  };
3964
4062
  var SELF_REFERENCING_FILES = /* @__PURE__ */ new Set(["todo-scanner.ts", "todo-scanner.test.ts"]);
3965
- function scanDirectory4(dir, projectRoot) {
4063
+ function scanDirectory4(dir, projectRoot, pattern) {
3966
4064
  const todos = [];
3967
4065
  try {
3968
4066
  for (const entry of readdirSync15(dir)) {
@@ -3971,22 +4069,22 @@ function scanDirectory4(dir, projectRoot) {
3971
4069
  const fullPath = join30(dir, entry);
3972
4070
  const stat = statSync16(fullPath);
3973
4071
  if (stat.isDirectory()) {
3974
- todos.push(...scanDirectory4(fullPath, projectRoot));
4072
+ todos.push(...scanDirectory4(fullPath, projectRoot, pattern));
3975
4073
  } else if (SOURCE_EXTENSIONS3.has(extname7(entry))) {
3976
- todos.push(...scanFile2(fullPath, projectRoot));
4074
+ todos.push(...scanFile2(fullPath, projectRoot, pattern));
3977
4075
  }
3978
4076
  }
3979
4077
  } catch {
3980
4078
  }
3981
4079
  return todos;
3982
4080
  }
3983
- function scanFile2(filePath, projectRoot) {
4081
+ function scanFile2(filePath, projectRoot, pattern) {
3984
4082
  const todos = [];
3985
4083
  try {
3986
4084
  const lines = readFileSync25(filePath, "utf-8").split("\n");
3987
4085
  const relPath = filePath.replace(projectRoot + "/", "");
3988
4086
  for (let i = 0; i < lines.length; i++) {
3989
- const match = stripStringLiterals(lines[i]).match(TODO_PATTERN);
4087
+ const match = stripStringLiterals(lines[i]).match(pattern);
3990
4088
  if (match) {
3991
4089
  todos.push({
3992
4090
  file: relPath,
@@ -4006,8 +4104,10 @@ var TypeScriptRunner = class extends BaseRunner {
4006
4104
  async isApplicable(projectPath) {
4007
4105
  return existsSync19(join31(projectPath, "tsconfig.json"));
4008
4106
  }
4009
- async run(projectPath) {
4107
+ async run(projectPath, options) {
4010
4108
  const elapsed = timer();
4109
+ const thresholds = options?.checkConfig?.thresholds;
4110
+ const maxErrors = thresholds?.maxErrors ?? 20;
4011
4111
  try {
4012
4112
  const { stdout, stderr } = await execa13("tsc", ["--noEmit", "--pretty", "false"], {
4013
4113
  cwd: projectPath,
@@ -4039,7 +4139,7 @@ var TypeScriptRunner = class extends BaseRunner {
4039
4139
  category: this.category,
4040
4140
  name: "Type Safety",
4041
4141
  score,
4042
- status: count === 0 ? "pass" : count > 20 ? "fail" : "warning",
4142
+ status: count === 0 ? "pass" : count > maxErrors ? "fail" : "warning",
4043
4143
  issues,
4044
4144
  toolsUsed: ["tsc"],
4045
4145
  duration: elapsed(),
@@ -4362,7 +4462,11 @@ async function runSickbay(options = {}) {
4362
4462
  const applicable = await runner.isApplicable(projectPath, context);
4363
4463
  if (!applicable) return null;
4364
4464
  options.onCheckStart?.(runner.name);
4365
- const result = await runner.run(projectPath, { verbose: options.verbose });
4465
+ const checkCfg = getCheckConfig(config, runner.name);
4466
+ const result = await runner.run(projectPath, {
4467
+ verbose: options.verbose,
4468
+ checkConfig: checkCfg ? { thresholds: checkCfg.thresholds, exclude: checkCfg.exclude } : void 0
4469
+ });
4366
4470
  options.onCheckComplete?.(result);
4367
4471
  return result;
4368
4472
  })
@@ -4478,6 +4582,7 @@ async function getDependencyTree(projectPath, packageManager) {
4478
4582
 
4479
4583
  export {
4480
4584
  defineConfig,
4585
+ getCheckConfig,
4481
4586
  isCheckDisabled,
4482
4587
  resolveConfigMeta,
4483
4588
  validateConfig,
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defineConfig
3
- } from "./chunk-EXMWNJFU.js";
3
+ } from "./chunk-NYE7UH3H.js";
4
4
  export {
5
5
  defineConfig
6
6
  };
@@ -12,6 +12,7 @@ import {
12
12
  detectPackageManager,
13
13
  detectProject,
14
14
  getAvailableChecks,
15
+ getCheckConfig,
15
16
  getDependencyTree,
16
17
  getScoreColor,
17
18
  getScoreEmoji,
@@ -21,7 +22,7 @@ import {
21
22
  runSickbay,
22
23
  runSickbayMonorepo,
23
24
  validateConfig
24
- } from "./chunk-EXMWNJFU.js";
25
+ } from "./chunk-NYE7UH3H.js";
25
26
  export {
26
27
  CRITICAL_LINES,
27
28
  SCORE_EXCELLENT,
@@ -36,6 +37,7 @@ export {
36
37
  detectPackageManager,
37
38
  detectProject,
38
39
  getAvailableChecks,
40
+ getCheckConfig,
39
41
  getDependencyTree,
40
42
  getScoreColor,
41
43
  getScoreEmoji,
package/dist/index.js CHANGED
@@ -8,12 +8,12 @@ import {
8
8
  } from "./chunk-TYG7ZQBP.js";
9
9
  import {
10
10
  Header
11
- } from "./chunk-DRBWOJSY.js";
11
+ } from "./chunk-EONIXEDB.js";
12
12
  import {
13
13
  getScoreEmoji,
14
14
  runSickbay,
15
15
  runSickbayMonorepo
16
- } from "./chunk-EXMWNJFU.js";
16
+ } from "./chunk-NYE7UH3H.js";
17
17
 
18
18
  // src/index.ts
19
19
  import { existsSync } from "fs";
@@ -176,7 +176,7 @@ function App({
176
176
  setMonorepoReport(r);
177
177
  setProjectName(`monorepo (${r.packages.length} packages)`);
178
178
  try {
179
- const { getDependencyTree } = await import("./dist-GBGEDULY.js");
179
+ const { getDependencyTree } = await import("./dist-PJQ2AQQL.js");
180
180
  const { saveDepTree } = await import("./history-XLNVZEDI.js");
181
181
  const packages = {};
182
182
  for (const pkg of r.packages) {
@@ -245,7 +245,7 @@ function App({
245
245
  } catch {
246
246
  }
247
247
  try {
248
- const { getDependencyTree } = await import("./dist-GBGEDULY.js");
248
+ const { getDependencyTree } = await import("./dist-PJQ2AQQL.js");
249
249
  const { saveDepTree } = await import("./history-XLNVZEDI.js");
250
250
  const tree = await getDependencyTree(projectPath, r.projectInfo.packageManager);
251
251
  saveDepTree(projectPath, tree);
@@ -303,7 +303,7 @@ if (existsSync(globalConfigPath)) {
303
303
  }
304
304
  config({ debug: false, quiet: true });
305
305
  var program = new Command();
306
- program.name("sickbay").description("React project health check CLI").version("1.7.3", "-v, --version").enablePositionalOptions().passThroughOptions().option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--package <name>", "scope to a single named package (monorepo only)").option("--json", "output raw JSON report").option("--web", "open web dashboard after scan").option("--no-ai", "disable AI features").option("--no-quotes", "suppress personality quotes in output").option("--verbose", "show verbose output").action(async (options) => {
306
+ program.name("sickbay").description("React project health check CLI").version("1.7.5", "-v, --version").enablePositionalOptions().passThroughOptions().option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--package <name>", "scope to a single named package (monorepo only)").option("--json", "output raw JSON report").option("--web", "open web dashboard after scan").option("--no-ai", "disable AI features").option("--no-quotes", "suppress personality quotes in output").option("--verbose", "show verbose output").action(async (options) => {
307
307
  if (options.path && options.path !== process.cwd()) {
308
308
  const projectEnvPath = join(options.path, ".env");
309
309
  if (existsSync(projectEnvPath)) {
@@ -313,13 +313,13 @@ program.name("sickbay").description("React project health check CLI").version("1
313
313
  const updatePromise = (async () => {
314
314
  try {
315
315
  const { checkForUpdate } = await import("./update-check-M7IFOMLJ.js");
316
- return await checkForUpdate("1.7.3");
316
+ return await checkForUpdate("1.7.5");
317
317
  } catch {
318
318
  return null;
319
319
  }
320
320
  })();
321
321
  const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
322
- const { detectMonorepo, runSickbay: runSickbay2, runSickbayMonorepo: runSickbayMonorepo2 } = await import("./dist-GBGEDULY.js");
322
+ const { detectMonorepo, runSickbay: runSickbay2, runSickbayMonorepo: runSickbayMonorepo2 } = await import("./dist-PJQ2AQQL.js");
323
323
  const monorepoInfo = await detectMonorepo(options.path);
324
324
  if (options.package && monorepoInfo.isMonorepo) {
325
325
  const { readFileSync } = await import("fs");
@@ -383,7 +383,7 @@ program.name("sickbay").description("React project health check CLI").version("1
383
383
  } catch {
384
384
  }
385
385
  try {
386
- const { getDependencyTree } = await import("./dist-GBGEDULY.js");
386
+ const { getDependencyTree } = await import("./dist-PJQ2AQQL.js");
387
387
  const { saveDepTree } = await import("./history-XLNVZEDI.js");
388
388
  const tree = await getDependencyTree(options.path, report.projectInfo.packageManager);
389
389
  saveDepTree(options.path, tree);
@@ -406,7 +406,7 @@ program.name("sickbay").description("React project health check CLI").version("1
406
406
  );
407
407
  });
408
408
  program.command("init").description("Initialize .sickbay/ folder and run an initial baseline scan").option("-p, --path <path>", "project path to initialize", process.cwd()).action(async (options) => {
409
- const { initSickbay } = await import("./init-Y6ZEUZBY.js");
409
+ const { initSickbay } = await import("./init-WBLDUGEB.js");
410
410
  await initSickbay(options.path);
411
411
  });
412
412
  program.command("fix").description("Interactively fix issues found by sickbay scan").option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--package <name>", "scope to a single package (monorepo only)").option("--all", "apply all available fixes without prompting").option("--dry-run", "show what would be fixed without executing").option("--verbose", "show verbose output").action(async (options) => {
@@ -416,9 +416,9 @@ program.command("fix").description("Interactively fix issues found by sickbay sc
416
416
  config({ path: projectEnvPath, override: true });
417
417
  }
418
418
  }
419
- const { resolveProject } = await import("./resolve-package-I6ECLCON.js");
419
+ const { resolveProject } = await import("./resolve-package-7S4ZB3OA.js");
420
420
  const resolution = await resolveProject(options.path, options.package);
421
- const { FixApp } = await import("./FixApp-AL2OABBZ.js");
421
+ const { FixApp } = await import("./FixApp-46TGLR3C.js");
422
422
  const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
423
423
  const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
424
424
  render(
@@ -441,10 +441,10 @@ program.command("trend").description("Show score history and trends over time").
441
441
  config({ path: projectEnvPath, override: true });
442
442
  }
443
443
  }
444
- const { resolveProject } = await import("./resolve-package-I6ECLCON.js");
444
+ const { resolveProject } = await import("./resolve-package-7S4ZB3OA.js");
445
445
  const resolution = await resolveProject(options.path, options.package);
446
446
  const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
447
- const { TrendApp } = await import("./TrendApp-XKUCHCA3.js");
447
+ const { TrendApp } = await import("./TrendApp-WMFR4L3A.js");
448
448
  render(
449
449
  React6.createElement(TrendApp, {
450
450
  projectPath,
@@ -463,10 +463,10 @@ program.command("stats").description("Show a quick codebase overview and project
463
463
  config({ path: projectEnvPath, override: true });
464
464
  }
465
465
  }
466
- const { resolveProject } = await import("./resolve-package-I6ECLCON.js");
466
+ const { resolveProject } = await import("./resolve-package-7S4ZB3OA.js");
467
467
  const resolution = await resolveProject(options.path, options.package);
468
468
  const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
469
- const { StatsApp } = await import("./StatsApp-IDK6NZ26.js");
469
+ const { StatsApp } = await import("./StatsApp-5YMWUNHQ.js");
470
470
  render(
471
471
  React6.createElement(StatsApp, {
472
472
  projectPath,
@@ -478,7 +478,7 @@ program.command("stats").description("Show a quick codebase overview and project
478
478
  );
479
479
  });
480
480
  program.command("tui").description("Launch the persistent developer dashboard").option("-p, --path <path>", "project path to monitor", process.cwd()).option("--no-watch", "disable file-watching auto-refresh").option("--no-quotes", "suppress personality quotes in output").option("--refresh <seconds>", "auto-refresh interval in seconds", "300").option("-c, --checks <checks>", "comma-separated list of checks to run").action(async (options) => {
481
- const { TuiApp } = await import("./TuiApp-7QFQPAVR.js");
481
+ const { TuiApp } = await import("./TuiApp-DDC2E3SZ.js");
482
482
  const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
483
483
  render(
484
484
  React6.createElement(TuiApp, {
@@ -498,10 +498,10 @@ program.command("doctor").description("Diagnose project setup and configuration
498
498
  config({ path: projectEnvPath, override: true });
499
499
  }
500
500
  }
501
- const { resolveProject } = await import("./resolve-package-I6ECLCON.js");
501
+ const { resolveProject } = await import("./resolve-package-7S4ZB3OA.js");
502
502
  const resolution = await resolveProject(options.path, options.package);
503
503
  const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
504
- const { DoctorApp } = await import("./DoctorApp-YTLQ2DS6.js");
504
+ const { DoctorApp } = await import("./DoctorApp-XVZMFDLM.js");
505
505
  render(
506
506
  React6.createElement(DoctorApp, {
507
507
  projectPath,
@@ -514,13 +514,13 @@ program.command("doctor").description("Diagnose project setup and configuration
514
514
  );
515
515
  });
516
516
  program.command("badge").description("Generate a health score badge for your README").option("-p, --path <path>", "project path", process.cwd()).option("--package <name>", "scope to a single package (monorepo only)").option("--html", "output HTML <img> tag instead of markdown").option("--url", "output bare badge URL only").option("--label <text>", "custom badge label", "sickbay").option("--scan", "run a fresh scan instead of using last report").action(async (options) => {
517
- const { resolveProject } = await import("./resolve-package-I6ECLCON.js");
517
+ const { resolveProject } = await import("./resolve-package-7S4ZB3OA.js");
518
518
  const resolution = await resolveProject(options.path, options.package);
519
519
  const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
520
520
  const { loadScoreFromLastReport, badgeUrl, badgeMarkdown, badgeHtml } = await import("./badge-KQ73KEIN.js");
521
521
  let score = options.scan ? null : loadScoreFromLastReport(projectPath);
522
522
  if (score === null) {
523
- const { runSickbay: runSickbay2 } = await import("./dist-GBGEDULY.js");
523
+ const { runSickbay: runSickbay2 } = await import("./dist-PJQ2AQQL.js");
524
524
  const report = await runSickbay2({ projectPath, quotes: false });
525
525
  try {
526
526
  const { saveEntry, saveLastReport } = await import("./history-XLNVZEDI.js");
@@ -549,7 +549,7 @@ program.command("diff <branch>").description("Compare health score against anoth
549
549
  }
550
550
  }
551
551
  const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
552
- const { DiffApp } = await import("./DiffApp-RLMGNCUO.js");
552
+ const { DiffApp } = await import("./DiffApp-CG2DRKTU.js");
553
553
  render(
554
554
  React6.createElement(DiffApp, {
555
555
  projectPath: options.path,
@@ -2,7 +2,7 @@ import {
2
2
  detectContext,
3
3
  getAvailableChecks,
4
4
  runSickbay
5
- } from "./chunk-EXMWNJFU.js";
5
+ } from "./chunk-NYE7UH3H.js";
6
6
  import {
7
7
  saveEntry
8
8
  } from "./chunk-3OR2GFVE.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  resolveProject,
3
3
  shortName
4
- } from "./chunk-QT7EP2BN.js";
5
- import "./chunk-EXMWNJFU.js";
4
+ } from "./chunk-LRBDCBJG.js";
5
+ import "./chunk-NYE7UH3H.js";
6
6
  export {
7
7
  resolveProject,
8
8
  shortName
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sickbay",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "Zero-config health check CLI for JavaScript and TypeScript projects",
5
5
  "license": "MIT",
6
6
  "repository": {