@zealakit/css-doctor-cli 1.0.0 → 1.1.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.
package/dist/index.js CHANGED
@@ -3052,80 +3052,6 @@ var require_commander = __commonJS({
3052
3052
  }
3053
3053
  });
3054
3054
 
3055
- // ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
3056
- var require_picocolors = __commonJS({
3057
- "../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports, module) {
3058
- "use strict";
3059
- init_esm_shims();
3060
- var p = process || {};
3061
- var argv = p.argv || [];
3062
- var env = p.env || {};
3063
- var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
3064
- var formatter = (open, close, replace = open) => (input) => {
3065
- let string = "" + input, index = string.indexOf(close, open.length);
3066
- return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
3067
- };
3068
- var replaceClose = (string, close, replace, index) => {
3069
- let result = "", cursor = 0;
3070
- do {
3071
- result += string.substring(cursor, index) + replace;
3072
- cursor = index + close.length;
3073
- index = string.indexOf(close, cursor);
3074
- } while (~index);
3075
- return result + string.substring(cursor);
3076
- };
3077
- var createColors = (enabled = isColorSupported) => {
3078
- let f = enabled ? formatter : () => String;
3079
- return {
3080
- isColorSupported: enabled,
3081
- reset: f("\x1B[0m", "\x1B[0m"),
3082
- bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
3083
- dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
3084
- italic: f("\x1B[3m", "\x1B[23m"),
3085
- underline: f("\x1B[4m", "\x1B[24m"),
3086
- inverse: f("\x1B[7m", "\x1B[27m"),
3087
- hidden: f("\x1B[8m", "\x1B[28m"),
3088
- strikethrough: f("\x1B[9m", "\x1B[29m"),
3089
- black: f("\x1B[30m", "\x1B[39m"),
3090
- red: f("\x1B[31m", "\x1B[39m"),
3091
- green: f("\x1B[32m", "\x1B[39m"),
3092
- yellow: f("\x1B[33m", "\x1B[39m"),
3093
- blue: f("\x1B[34m", "\x1B[39m"),
3094
- magenta: f("\x1B[35m", "\x1B[39m"),
3095
- cyan: f("\x1B[36m", "\x1B[39m"),
3096
- white: f("\x1B[37m", "\x1B[39m"),
3097
- gray: f("\x1B[90m", "\x1B[39m"),
3098
- bgBlack: f("\x1B[40m", "\x1B[49m"),
3099
- bgRed: f("\x1B[41m", "\x1B[49m"),
3100
- bgGreen: f("\x1B[42m", "\x1B[49m"),
3101
- bgYellow: f("\x1B[43m", "\x1B[49m"),
3102
- bgBlue: f("\x1B[44m", "\x1B[49m"),
3103
- bgMagenta: f("\x1B[45m", "\x1B[49m"),
3104
- bgCyan: f("\x1B[46m", "\x1B[49m"),
3105
- bgWhite: f("\x1B[47m", "\x1B[49m"),
3106
- blackBright: f("\x1B[90m", "\x1B[39m"),
3107
- redBright: f("\x1B[91m", "\x1B[39m"),
3108
- greenBright: f("\x1B[92m", "\x1B[39m"),
3109
- yellowBright: f("\x1B[93m", "\x1B[39m"),
3110
- blueBright: f("\x1B[94m", "\x1B[39m"),
3111
- magentaBright: f("\x1B[95m", "\x1B[39m"),
3112
- cyanBright: f("\x1B[96m", "\x1B[39m"),
3113
- whiteBright: f("\x1B[97m", "\x1B[39m"),
3114
- bgBlackBright: f("\x1B[100m", "\x1B[49m"),
3115
- bgRedBright: f("\x1B[101m", "\x1B[49m"),
3116
- bgGreenBright: f("\x1B[102m", "\x1B[49m"),
3117
- bgYellowBright: f("\x1B[103m", "\x1B[49m"),
3118
- bgBlueBright: f("\x1B[104m", "\x1B[49m"),
3119
- bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
3120
- bgCyanBright: f("\x1B[106m", "\x1B[49m"),
3121
- bgWhiteBright: f("\x1B[107m", "\x1B[49m")
3122
- };
3123
- };
3124
- module.exports = createColors();
3125
- module.exports.createColors = createColors;
3126
- }
3127
- });
3128
-
3129
3055
  // ../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/utils/array.js
3130
3056
  var require_array = __commonJS({
3131
3057
  "../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/utils/array.js"(exports) {
@@ -3803,7 +3729,7 @@ var require_fill_range = __commonJS({
3803
3729
  }
3804
3730
  return options.stringify === true;
3805
3731
  };
3806
- var pad = (input, maxLength, toNumber) => {
3732
+ var pad4 = (input, maxLength, toNumber) => {
3807
3733
  if (maxLength > 0) {
3808
3734
  let dash = input[0] === "-" ? "-" : "";
3809
3735
  if (dash) input = input.slice(1);
@@ -3905,7 +3831,7 @@ var require_fill_range = __commonJS({
3905
3831
  if (options.toRegex === true && step > 1) {
3906
3832
  push(a);
3907
3833
  } else {
3908
- range.push(pad(format(a, index), maxLen, toNumber));
3834
+ range.push(pad4(format(a, index), maxLen, toNumber));
3909
3835
  }
3910
3836
  a = descending ? a - step : a + step;
3911
3837
  index++;
@@ -6583,7 +6509,7 @@ var require_pattern = __commonJS({
6583
6509
  const absolute = [];
6584
6510
  const relative2 = [];
6585
6511
  for (const pattern of patterns) {
6586
- if (isAbsolute2(pattern)) {
6512
+ if (isAbsolute3(pattern)) {
6587
6513
  absolute.push(pattern);
6588
6514
  } else {
6589
6515
  relative2.push(pattern);
@@ -6592,10 +6518,10 @@ var require_pattern = __commonJS({
6592
6518
  return [absolute, relative2];
6593
6519
  }
6594
6520
  exports.partitionAbsoluteAndRelative = partitionAbsoluteAndRelative;
6595
- function isAbsolute2(pattern) {
6521
+ function isAbsolute3(pattern) {
6596
6522
  return path2.isAbsolute(pattern);
6597
6523
  }
6598
- exports.isAbsolute = isAbsolute2;
6524
+ exports.isAbsolute = isAbsolute3;
6599
6525
  }
6600
6526
  });
6601
6527
 
@@ -6899,7 +6825,7 @@ var require_async = __commonJS({
6899
6825
  callSuccessCallback(callback, lstat);
6900
6826
  return;
6901
6827
  }
6902
- settings.fs.stat(path2, (statError, stat2) => {
6828
+ settings.fs.stat(path2, (statError, stat3) => {
6903
6829
  if (statError !== null) {
6904
6830
  if (settings.throwErrorOnBrokenSymbolicLink) {
6905
6831
  callFailureCallback(callback, statError);
@@ -6909,9 +6835,9 @@ var require_async = __commonJS({
6909
6835
  return;
6910
6836
  }
6911
6837
  if (settings.markSymbolicLink) {
6912
- stat2.isSymbolicLink = () => true;
6838
+ stat3.isSymbolicLink = () => true;
6913
6839
  }
6914
- callSuccessCallback(callback, stat2);
6840
+ callSuccessCallback(callback, stat3);
6915
6841
  });
6916
6842
  });
6917
6843
  }
@@ -6938,11 +6864,11 @@ var require_sync = __commonJS({
6938
6864
  return lstat;
6939
6865
  }
6940
6866
  try {
6941
- const stat2 = settings.fs.statSync(path2);
6867
+ const stat3 = settings.fs.statSync(path2);
6942
6868
  if (settings.markSymbolicLink) {
6943
- stat2.isSymbolicLink = () => true;
6869
+ stat3.isSymbolicLink = () => true;
6944
6870
  }
6945
- return stat2;
6871
+ return stat3;
6946
6872
  } catch (error) {
6947
6873
  if (!settings.throwErrorOnBrokenSymbolicLink) {
6948
6874
  return lstat;
@@ -7012,14 +6938,14 @@ var require_out = __commonJS({
7012
6938
  var sync = require_sync();
7013
6939
  var settings_1 = require_settings();
7014
6940
  exports.Settings = settings_1.default;
7015
- function stat2(path2, optionsOrSettingsOrCallback, callback) {
6941
+ function stat3(path2, optionsOrSettingsOrCallback, callback) {
7016
6942
  if (typeof optionsOrSettingsOrCallback === "function") {
7017
6943
  async.read(path2, getSettings(), optionsOrSettingsOrCallback);
7018
6944
  return;
7019
6945
  }
7020
6946
  async.read(path2, getSettings(optionsOrSettingsOrCallback), callback);
7021
6947
  }
7022
- exports.stat = stat2;
6948
+ exports.stat = stat3;
7023
6949
  function statSync(path2, optionsOrSettings) {
7024
6950
  const settings = getSettings(optionsOrSettings);
7025
6951
  return sync.read(path2, settings);
@@ -7705,41 +7631,41 @@ var require_queue = __commonJS({
7705
7631
  queue.drained = drained;
7706
7632
  return queue;
7707
7633
  function push(value) {
7708
- var p = new Promise(function(resolve3, reject) {
7634
+ var p = new Promise(function(resolve9, reject) {
7709
7635
  pushCb(value, function(err, result) {
7710
7636
  if (err) {
7711
7637
  reject(err);
7712
7638
  return;
7713
7639
  }
7714
- resolve3(result);
7640
+ resolve9(result);
7715
7641
  });
7716
7642
  });
7717
7643
  p.catch(noop);
7718
7644
  return p;
7719
7645
  }
7720
7646
  function unshift(value) {
7721
- var p = new Promise(function(resolve3, reject) {
7647
+ var p = new Promise(function(resolve9, reject) {
7722
7648
  unshiftCb(value, function(err, result) {
7723
7649
  if (err) {
7724
7650
  reject(err);
7725
7651
  return;
7726
7652
  }
7727
- resolve3(result);
7653
+ resolve9(result);
7728
7654
  });
7729
7655
  });
7730
7656
  p.catch(noop);
7731
7657
  return p;
7732
7658
  }
7733
7659
  function drained() {
7734
- var p = new Promise(function(resolve3) {
7660
+ var p = new Promise(function(resolve9) {
7735
7661
  process.nextTick(function() {
7736
7662
  if (queue.idle()) {
7737
- resolve3();
7663
+ resolve9();
7738
7664
  } else {
7739
7665
  var previousDrain = queue.drain;
7740
7666
  queue.drain = function() {
7741
7667
  if (typeof previousDrain === "function") previousDrain();
7742
- resolve3();
7668
+ resolve9();
7743
7669
  queue.drain = previousDrain;
7744
7670
  };
7745
7671
  }
@@ -8236,9 +8162,9 @@ var require_stream3 = __commonJS({
8236
8162
  });
8237
8163
  }
8238
8164
  _getStat(filepath) {
8239
- return new Promise((resolve3, reject) => {
8165
+ return new Promise((resolve9, reject) => {
8240
8166
  this._stat(filepath, this._fsStatSettings, (error, stats) => {
8241
- return error === null ? resolve3(stats) : reject(error);
8167
+ return error === null ? resolve9(stats) : reject(error);
8242
8168
  });
8243
8169
  });
8244
8170
  }
@@ -8263,10 +8189,10 @@ var require_async5 = __commonJS({
8263
8189
  this._readerStream = new stream_1.default(this._settings);
8264
8190
  }
8265
8191
  dynamic(root, options) {
8266
- return new Promise((resolve3, reject) => {
8192
+ return new Promise((resolve9, reject) => {
8267
8193
  this._walkAsync(root, options, (error, entries) => {
8268
8194
  if (error === null) {
8269
- resolve3(entries);
8195
+ resolve9(entries);
8270
8196
  } else {
8271
8197
  reject(error);
8272
8198
  }
@@ -8276,10 +8202,10 @@ var require_async5 = __commonJS({
8276
8202
  async static(patterns, options) {
8277
8203
  const entries = [];
8278
8204
  const stream = this._readerStream.static(patterns, options);
8279
- return new Promise((resolve3, reject) => {
8205
+ return new Promise((resolve9, reject) => {
8280
8206
  stream.once("error", reject);
8281
8207
  stream.on("data", (entry) => entries.push(entry));
8282
- stream.once("end", () => resolve3(entries));
8208
+ stream.once("end", () => resolve9(entries));
8283
8209
  });
8284
8210
  }
8285
8211
  };
@@ -8949,6 +8875,80 @@ var require_out4 = __commonJS({
8949
8875
  }
8950
8876
  });
8951
8877
 
8878
+ // ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
8879
+ var require_picocolors = __commonJS({
8880
+ "../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports, module) {
8881
+ "use strict";
8882
+ init_esm_shims();
8883
+ var p = process || {};
8884
+ var argv = p.argv || [];
8885
+ var env = p.env || {};
8886
+ var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
8887
+ var formatter = (open, close, replace = open) => (input) => {
8888
+ let string = "" + input, index = string.indexOf(close, open.length);
8889
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
8890
+ };
8891
+ var replaceClose = (string, close, replace, index) => {
8892
+ let result = "", cursor = 0;
8893
+ do {
8894
+ result += string.substring(cursor, index) + replace;
8895
+ cursor = index + close.length;
8896
+ index = string.indexOf(close, cursor);
8897
+ } while (~index);
8898
+ return result + string.substring(cursor);
8899
+ };
8900
+ var createColors = (enabled = isColorSupported) => {
8901
+ let f = enabled ? formatter : () => String;
8902
+ return {
8903
+ isColorSupported: enabled,
8904
+ reset: f("\x1B[0m", "\x1B[0m"),
8905
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
8906
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
8907
+ italic: f("\x1B[3m", "\x1B[23m"),
8908
+ underline: f("\x1B[4m", "\x1B[24m"),
8909
+ inverse: f("\x1B[7m", "\x1B[27m"),
8910
+ hidden: f("\x1B[8m", "\x1B[28m"),
8911
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
8912
+ black: f("\x1B[30m", "\x1B[39m"),
8913
+ red: f("\x1B[31m", "\x1B[39m"),
8914
+ green: f("\x1B[32m", "\x1B[39m"),
8915
+ yellow: f("\x1B[33m", "\x1B[39m"),
8916
+ blue: f("\x1B[34m", "\x1B[39m"),
8917
+ magenta: f("\x1B[35m", "\x1B[39m"),
8918
+ cyan: f("\x1B[36m", "\x1B[39m"),
8919
+ white: f("\x1B[37m", "\x1B[39m"),
8920
+ gray: f("\x1B[90m", "\x1B[39m"),
8921
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
8922
+ bgRed: f("\x1B[41m", "\x1B[49m"),
8923
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
8924
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
8925
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
8926
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
8927
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
8928
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
8929
+ blackBright: f("\x1B[90m", "\x1B[39m"),
8930
+ redBright: f("\x1B[91m", "\x1B[39m"),
8931
+ greenBright: f("\x1B[92m", "\x1B[39m"),
8932
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
8933
+ blueBright: f("\x1B[94m", "\x1B[39m"),
8934
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
8935
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
8936
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
8937
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
8938
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
8939
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
8940
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
8941
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
8942
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
8943
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
8944
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
8945
+ };
8946
+ };
8947
+ module.exports = createColors();
8948
+ module.exports.createColors = createColors;
8949
+ }
8950
+ });
8951
+
8952
8952
  // src/index.ts
8953
8953
  init_esm_shims();
8954
8954
 
@@ -8973,8 +8973,30 @@ var {
8973
8973
  // package.json
8974
8974
  var package_default = {
8975
8975
  name: "@zealakit/css-doctor-cli",
8976
- version: "1.0.0",
8976
+ version: "1.1.0",
8977
8977
  description: "Command line interface for css-doctor: CSS diagnostics from the terminal",
8978
+ keywords: [
8979
+ "css",
8980
+ "css-doctor",
8981
+ "cli",
8982
+ "command-line",
8983
+ "css-analysis",
8984
+ "css-optimizer",
8985
+ "dead-code-elimination",
8986
+ "unused-css",
8987
+ "purgecss",
8988
+ "css-health",
8989
+ "css-baseline",
8990
+ "css-regression",
8991
+ "css-splitting",
8992
+ "page-coverage",
8993
+ "devtools",
8994
+ "ci",
8995
+ "performance",
8996
+ "web-performance",
8997
+ "stylesheet",
8998
+ "frontend"
8999
+ ],
8978
9000
  author: "nqhed - Huy Edward Nguyen",
8979
9001
  type: "module",
8980
9002
  license: "MIT",
@@ -9027,11 +9049,9 @@ var package_default = {
9027
9049
  homepage: "https://github.com/zealakit/css-doctor#readme"
9028
9050
  };
9029
9051
 
9030
- // src/commands/check.ts
9052
+ // src/commands/baseline.ts
9031
9053
  init_esm_shims();
9032
- import { createRequire } from "module";
9033
- import { performance } from "perf_hooks";
9034
- import { resolve as resolve2 } from "path";
9054
+ import { resolve as resolve3 } from "path";
9035
9055
 
9036
9056
  // src/formatters/json.ts
9037
9057
  init_esm_shims();
@@ -9039,309 +9059,2734 @@ function formatJsonReport(report) {
9039
9059
  return JSON.stringify(report, null, 2);
9040
9060
  }
9041
9061
 
9042
- // src/formatters/terminal.ts
9062
+ // src/utils/baseline-io.ts
9043
9063
  init_esm_shims();
9044
- var import_picocolors = __toESM(require_picocolors(), 1);
9045
- var TOP_SELECTOR_LIMIT = 5;
9046
- function terminalExitCode(report) {
9047
- return report.success ? 0 : 1;
9064
+ import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
9065
+ import { dirname } from "path";
9066
+
9067
+ // src/utils/binding.ts
9068
+ init_esm_shims();
9069
+ import { createRequire } from "module";
9070
+
9071
+ // src/utils/file.ts
9072
+ init_esm_shims();
9073
+ var import_fast_glob = __toESM(require_out4(), 1);
9074
+ import { readFile, stat } from "fs/promises";
9075
+ import { isAbsolute, relative, resolve, sep } from "path";
9076
+ var DEFAULT_IGNORES = ["**/node_modules/**", "**/.git/**"];
9077
+ async function isFile(filePath) {
9078
+ try {
9079
+ return (await stat(filePath)).isFile();
9080
+ } catch {
9081
+ return false;
9082
+ }
9048
9083
  }
9049
- function formatTerminalReport(report, options = {}) {
9050
- const colors = import_picocolors.default.createColors(
9051
- options.color ?? import_picocolors.default.isColorSupported
9052
- );
9053
- const lines = [];
9054
- const separator = colors.dim("\u2500".repeat(64));
9055
- lines.push(
9056
- `${colors.bold(colors.cyan("css-doctor"))} ${colors.dim(`v${report.version}`)} ${colors.dim("\xB7")} ${colors.dim("check")}`
9057
- );
9058
- lines.push(
9059
- `${colors.dim("patterns")} ${report.patterns.length > 0 ? report.patterns.join(", ") : "(none)"}`
9060
- );
9061
- lines.push(separator);
9062
- if (report.errors.length > 0) {
9063
- lines.push(colors.red(colors.bold("ERRORS")));
9064
- for (const error of report.errors) {
9065
- lines.push(` ${colors.red("\u2716")} ${error}`);
9066
- }
9067
- lines.push("");
9084
+ async function isDirectory(directoryPath) {
9085
+ try {
9086
+ return (await stat(directoryPath)).isDirectory();
9087
+ } catch {
9088
+ return false;
9068
9089
  }
9069
- for (const file of report.files) {
9070
- lines.push(...formatFile(file, colors));
9090
+ }
9091
+ async function resolveCssFiles(patterns, cwd) {
9092
+ const files = /* @__PURE__ */ new Set();
9093
+ const globPatterns = [];
9094
+ for (const pattern of patterns) {
9095
+ const absolutePath = isAbsolute(pattern) ? pattern : resolve(cwd, pattern);
9096
+ if (await isFile(absolutePath)) {
9097
+ files.add(absolutePath);
9098
+ } else {
9099
+ globPatterns.push(pattern);
9100
+ }
9071
9101
  }
9072
- if (report.files.length === 0 && report.errors.length === 0) {
9073
- lines.push(colors.yellow("No files were analyzed."));
9074
- lines.push("");
9102
+ if (globPatterns.length > 0) {
9103
+ const matches = await (0, import_fast_glob.default)(globPatterns, {
9104
+ cwd,
9105
+ absolute: true,
9106
+ onlyFiles: true,
9107
+ unique: true,
9108
+ dot: false,
9109
+ followSymbolicLinks: true,
9110
+ ignore: DEFAULT_IGNORES
9111
+ });
9112
+ for (const match of matches) {
9113
+ files.add(resolve(match));
9114
+ }
9075
9115
  }
9076
- lines.push(separator);
9077
- lines.push(...formatSummary(report, colors));
9078
- return lines.join("\n");
9116
+ return [...files].sort((left, right) => left.localeCompare(right));
9079
9117
  }
9080
- function formatFile(file, colors) {
9081
- const status = fileStatus(file);
9082
- const lines = [];
9083
- lines.push(
9084
- `${statusBadge(status, colors)} ${colors.bold(file.path)} ${colors.dim(`\xB7 ${formatBytes(file.sizeBytes)}`)}`
9085
- );
9086
- if (!file.ok) {
9087
- lines.push(` ${colors.red(file.error ?? "Unknown error")}`);
9088
- lines.push("");
9089
- return lines;
9118
+ async function readCssFile(filePath) {
9119
+ try {
9120
+ const buffer = await readFile(filePath);
9121
+ return {
9122
+ ok: true,
9123
+ content: buffer.toString("utf8"),
9124
+ sizeBytes: buffer.byteLength
9125
+ };
9126
+ } catch (error) {
9127
+ return {
9128
+ ok: false,
9129
+ error: `Failed to read "${filePath}": ${describeError(error)}`
9130
+ };
9090
9131
  }
9091
- const result = file.result;
9092
- if (!result) {
9093
- lines.push(` ${colors.red("No analysis result was returned.")}`);
9094
- lines.push("");
9132
+ }
9133
+ function toDisplayPath(filePath, cwd) {
9134
+ const relativePath = relative(cwd, filePath);
9135
+ const displayPath = relativePath.length > 0 ? relativePath : filePath;
9136
+ return displayPath.split(sep).join("/");
9137
+ }
9138
+ function describeError(error) {
9139
+ if (error instanceof Error) {
9140
+ return error.message;
9141
+ }
9142
+ return String(error);
9143
+ }
9144
+
9145
+ // src/utils/binding.ts
9146
+ var require2 = createRequire(import.meta.url);
9147
+ var cachedBinding;
9148
+ function loadBinding() {
9149
+ if (cachedBinding) {
9150
+ return cachedBinding;
9151
+ }
9152
+ try {
9153
+ cachedBinding = require2("@zealakit/css-doctor-binding");
9154
+ return cachedBinding;
9155
+ } catch (error) {
9156
+ throw new Error(
9157
+ "Unable to load @zealakit/css-doctor-binding. Build it first with `pnpm --filter @zealakit/css-doctor-binding build`.\n" + describeError(error)
9158
+ );
9159
+ }
9160
+ }
9161
+
9162
+ // src/utils/git-metadata.ts
9163
+ init_esm_shims();
9164
+ import { spawnSync } from "child_process";
9165
+ function readGitMetadata(cwd) {
9166
+ try {
9167
+ const commit = runGit(cwd, ["rev-parse", "HEAD"]);
9168
+ const branch = runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
9169
+ const status = runGit(cwd, ["status", "--porcelain"]);
9170
+ if (!commit && !branch) {
9171
+ return {};
9172
+ }
9173
+ return {
9174
+ commit: commit || void 0,
9175
+ branch: branch || void 0,
9176
+ dirty: status === void 0 ? void 0 : status.length > 0
9177
+ };
9178
+ } catch {
9179
+ return {};
9180
+ }
9181
+ }
9182
+ function runGit(cwd, args) {
9183
+ const result = spawnSync("git", args, {
9184
+ cwd,
9185
+ encoding: "utf8",
9186
+ stdio: ["ignore", "pipe", "ignore"]
9187
+ });
9188
+ if (result.status !== 0 || typeof result.stdout !== "string") {
9189
+ return void 0;
9190
+ }
9191
+ return result.stdout.trim();
9192
+ }
9193
+
9194
+ // src/utils/baseline-io.ts
9195
+ async function createBaselineSnapshot(options) {
9196
+ const cwd = options.cwd;
9197
+ const errors = [];
9198
+ if (options.patterns.length === 0) {
9199
+ return {
9200
+ ok: false,
9201
+ errors: [
9202
+ 'No CSS files or glob patterns were provided. Example: css-doctor baseline create "src/**/*.css"'
9203
+ ]
9204
+ };
9205
+ }
9206
+ if (!await isDirectory(cwd)) {
9207
+ return {
9208
+ ok: false,
9209
+ errors: [
9210
+ `Working directory does not exist or is not a directory: ${cwd}`
9211
+ ]
9212
+ };
9213
+ }
9214
+ let files = [];
9215
+ try {
9216
+ files = await resolveCssFiles(options.patterns, cwd);
9217
+ } catch (error) {
9218
+ return {
9219
+ ok: false,
9220
+ errors: [`Failed to resolve CSS files: ${describeError(error)}`]
9221
+ };
9222
+ }
9223
+ if (files.length === 0) {
9224
+ return {
9225
+ ok: false,
9226
+ errors: [`No CSS files matched: ${options.patterns.join(", ")}`]
9227
+ };
9228
+ }
9229
+ try {
9230
+ const binding = loadBinding();
9231
+ const batchFiles = [];
9232
+ for (const file of files) {
9233
+ const read = await readCssFile(file);
9234
+ if (!read.ok) {
9235
+ errors.push(`${toDisplayPath(file, cwd)}: ${read.error}`);
9236
+ continue;
9237
+ }
9238
+ batchFiles.push({
9239
+ path: toDisplayPath(file, cwd),
9240
+ cssContent: read.content,
9241
+ sizeBytes: read.sizeBytes
9242
+ });
9243
+ }
9244
+ if (batchFiles.length === 0) {
9245
+ return {
9246
+ ok: false,
9247
+ errors: errors.length > 0 ? errors : ["No CSS files could be read for baseline creation."]
9248
+ };
9249
+ }
9250
+ const git = options.includeGit === false ? void 0 : readGitMetadata(cwd);
9251
+ const json = binding.createBaseline({
9252
+ files: batchFiles,
9253
+ toolVersion: package_default.version,
9254
+ createdAt: options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
9255
+ patterns: options.patterns,
9256
+ threshold: options.threshold ?? void 0,
9257
+ project: {
9258
+ root: "."
9259
+ },
9260
+ git
9261
+ });
9262
+ const document = JSON.parse(json);
9263
+ return {
9264
+ ok: true,
9265
+ json,
9266
+ document,
9267
+ files: batchFiles.length
9268
+ };
9269
+ } catch (error) {
9270
+ return {
9271
+ ok: false,
9272
+ errors: [...errors, describeError(error)]
9273
+ };
9274
+ }
9275
+ }
9276
+ async function readBaselineFile(absolutePath) {
9277
+ try {
9278
+ const json = await readFile2(absolutePath, "utf8");
9279
+ return { ok: true, json };
9280
+ } catch (error) {
9281
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
9282
+ return {
9283
+ ok: false,
9284
+ error: `Baseline not found at ${absolutePath}. Create one with \`css-doctor baseline create\`.`
9285
+ };
9286
+ }
9287
+ return {
9288
+ ok: false,
9289
+ error: `Failed to read baseline ${absolutePath}: ${describeError(error)}`
9290
+ };
9291
+ }
9292
+ }
9293
+ async function writeBaselineFile(absolutePath, json) {
9294
+ try {
9295
+ await mkdir(dirname(absolutePath), { recursive: true });
9296
+ await writeFile(absolutePath, `${json.trimEnd()}
9297
+ `, "utf8");
9298
+ return { ok: true };
9299
+ } catch (error) {
9300
+ return {
9301
+ ok: false,
9302
+ error: `Failed to write baseline ${absolutePath}: ${describeError(error)}`
9303
+ };
9304
+ }
9305
+ }
9306
+ async function removeBaselineFile(absolutePath) {
9307
+ try {
9308
+ await unlink(absolutePath);
9309
+ return { ok: true };
9310
+ } catch (error) {
9311
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
9312
+ return {
9313
+ ok: false,
9314
+ error: `Baseline not found at ${absolutePath}.`
9315
+ };
9316
+ }
9317
+ return {
9318
+ ok: false,
9319
+ error: `Failed to remove baseline ${absolutePath}: ${describeError(error)}`
9320
+ };
9321
+ }
9322
+ }
9323
+ function validateBaselineJson(json, toolVersion = package_default.version) {
9324
+ const binding = loadBinding();
9325
+ return binding.validateBaseline(json, toolVersion);
9326
+ }
9327
+
9328
+ // src/utils/config.ts
9329
+ init_esm_shims();
9330
+ import { readFile as readFile3 } from "fs/promises";
9331
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
9332
+ var DEFAULT_BASELINE_PATH = ".css-doctor/baseline.json";
9333
+ var DEFAULT_CONFIG_PATH = ".css-doctor/config.json";
9334
+ var DEFAULT_CONFIG = {
9335
+ baseline: DEFAULT_BASELINE_PATH,
9336
+ patterns: [],
9337
+ threshold: null,
9338
+ regression: {
9339
+ enabled: true,
9340
+ scoreTolerance: 1,
9341
+ sizeIncreasePercent: 10,
9342
+ sizeAbsoluteToleranceBytes: 1024,
9343
+ maxNewErrors: null,
9344
+ maxNewWarnings: null
9345
+ },
9346
+ qualityGates: {
9347
+ minScore: null,
9348
+ maxCssSize: null,
9349
+ maxSizeIncreasePercent: null,
9350
+ maxNewErrors: null,
9351
+ maxNewWarnings: null,
9352
+ failOnRegression: false,
9353
+ failOnNewErrors: false,
9354
+ failOnNewDiagnostics: false
9355
+ }
9356
+ };
9357
+ async function loadProjectConfig(cwd, configPath) {
9358
+ const absoluteConfigPath = resolve2(cwd, configPath ?? DEFAULT_CONFIG_PATH);
9359
+ const errors = [];
9360
+ try {
9361
+ const raw = await readFile3(absoluteConfigPath, "utf8");
9362
+ const parsed = JSON.parse(raw);
9363
+ const config = normalizeConfig(parsed, errors);
9364
+ return {
9365
+ config,
9366
+ path: absoluteConfigPath,
9367
+ errors
9368
+ };
9369
+ } catch (error) {
9370
+ if (isMissingFileError(error)) {
9371
+ return {
9372
+ config: structuredClone(DEFAULT_CONFIG),
9373
+ path: null,
9374
+ errors: []
9375
+ };
9376
+ }
9377
+ return {
9378
+ config: structuredClone(DEFAULT_CONFIG),
9379
+ path: absoluteConfigPath,
9380
+ errors: [
9381
+ `Failed to load config ${absoluteConfigPath}: ${describeError(error)}`
9382
+ ]
9383
+ };
9384
+ }
9385
+ }
9386
+ function resolveBaselinePath(cwd, baselinePath) {
9387
+ return isAbsolute2(baselinePath) ? baselinePath : resolve2(cwd, baselinePath);
9388
+ }
9389
+ function normalizeConfig(value, errors) {
9390
+ const config = structuredClone(DEFAULT_CONFIG);
9391
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
9392
+ errors.push("Config root must be a JSON object.");
9393
+ return config;
9394
+ }
9395
+ const root = value;
9396
+ if (root.baseline !== void 0) {
9397
+ if (typeof root.baseline !== "string" || root.baseline.trim() === "") {
9398
+ errors.push("`baseline` must be a non-empty string path.");
9399
+ } else {
9400
+ config.baseline = root.baseline;
9401
+ }
9402
+ }
9403
+ if (root.patterns !== void 0) {
9404
+ if (!Array.isArray(root.patterns) || root.patterns.some((pattern) => typeof pattern !== "string")) {
9405
+ errors.push("`patterns` must be an array of strings.");
9406
+ } else {
9407
+ config.patterns = root.patterns;
9408
+ }
9409
+ }
9410
+ if (root.threshold !== void 0) {
9411
+ if (typeof root.threshold !== "number" || !Number.isInteger(root.threshold) || root.threshold < 0) {
9412
+ errors.push("`threshold` must be a non-negative integer.");
9413
+ } else {
9414
+ config.threshold = root.threshold;
9415
+ }
9416
+ }
9417
+ if (root.regression !== void 0) {
9418
+ Object.assign(
9419
+ config.regression,
9420
+ normalizeRegression(root.regression, errors)
9421
+ );
9422
+ }
9423
+ if (root.qualityGates !== void 0) {
9424
+ Object.assign(
9425
+ config.qualityGates,
9426
+ normalizeQualityGates(root.qualityGates, errors)
9427
+ );
9428
+ }
9429
+ return config;
9430
+ }
9431
+ function normalizeRegression(value, errors) {
9432
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
9433
+ errors.push("`regression` must be an object.");
9434
+ return {};
9435
+ }
9436
+ const source = value;
9437
+ const result = {};
9438
+ if (source.enabled !== void 0) {
9439
+ if (typeof source.enabled !== "boolean") {
9440
+ errors.push("`regression.enabled` must be a boolean.");
9441
+ } else {
9442
+ result.enabled = source.enabled;
9443
+ }
9444
+ }
9445
+ assignNumber(
9446
+ source,
9447
+ "scoreTolerance",
9448
+ result,
9449
+ "scoreTolerance",
9450
+ errors,
9451
+ "`regression.scoreTolerance`",
9452
+ { integer: true, min: 0, max: 100 }
9453
+ );
9454
+ assignNumber(
9455
+ source,
9456
+ "sizeIncreasePercent",
9457
+ result,
9458
+ "sizeIncreasePercent",
9459
+ errors,
9460
+ "`regression.sizeIncreasePercent`",
9461
+ { min: 0 }
9462
+ );
9463
+ assignNumber(
9464
+ source,
9465
+ "sizeAbsoluteToleranceBytes",
9466
+ result,
9467
+ "sizeAbsoluteToleranceBytes",
9468
+ errors,
9469
+ "`regression.sizeAbsoluteToleranceBytes`",
9470
+ { integer: true, min: 0 }
9471
+ );
9472
+ assignNullableInteger(
9473
+ source,
9474
+ "maxNewErrors",
9475
+ result,
9476
+ "maxNewErrors",
9477
+ errors,
9478
+ "`regression.maxNewErrors`"
9479
+ );
9480
+ assignNullableInteger(
9481
+ source,
9482
+ "maxNewWarnings",
9483
+ result,
9484
+ "maxNewWarnings",
9485
+ errors,
9486
+ "`regression.maxNewWarnings`"
9487
+ );
9488
+ return result;
9489
+ }
9490
+ function normalizeQualityGates(value, errors) {
9491
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
9492
+ errors.push("`qualityGates` must be an object.");
9493
+ return {};
9494
+ }
9495
+ const source = value;
9496
+ const result = {};
9497
+ assignNullableInteger(
9498
+ source,
9499
+ "minScore",
9500
+ result,
9501
+ "minScore",
9502
+ errors,
9503
+ "`qualityGates.minScore`",
9504
+ { max: 100 }
9505
+ );
9506
+ assignNullableInteger(
9507
+ source,
9508
+ "maxCssSize",
9509
+ result,
9510
+ "maxCssSize",
9511
+ errors,
9512
+ "`qualityGates.maxCssSize`"
9513
+ );
9514
+ assignNumber(
9515
+ source,
9516
+ "maxSizeIncreasePercent",
9517
+ result,
9518
+ "maxSizeIncreasePercent",
9519
+ errors,
9520
+ "`qualityGates.maxSizeIncreasePercent`",
9521
+ { min: 0, nullable: true }
9522
+ );
9523
+ assignNullableInteger(
9524
+ source,
9525
+ "maxNewErrors",
9526
+ result,
9527
+ "maxNewErrors",
9528
+ errors,
9529
+ "`qualityGates.maxNewErrors`"
9530
+ );
9531
+ assignNullableInteger(
9532
+ source,
9533
+ "maxNewWarnings",
9534
+ result,
9535
+ "maxNewWarnings",
9536
+ errors,
9537
+ "`qualityGates.maxNewWarnings`"
9538
+ );
9539
+ for (const key of [
9540
+ "failOnRegression",
9541
+ "failOnNewErrors",
9542
+ "failOnNewDiagnostics"
9543
+ ]) {
9544
+ if (source[key] !== void 0) {
9545
+ if (typeof source[key] !== "boolean") {
9546
+ errors.push(`\`qualityGates.${key}\` must be a boolean.`);
9547
+ } else {
9548
+ result[key] = source[key];
9549
+ }
9550
+ }
9551
+ }
9552
+ return result;
9553
+ }
9554
+ function assignNumber(source, sourceKey, target, targetKey, errors, label3, options = {}) {
9555
+ if (source[sourceKey] === void 0) {
9556
+ return;
9557
+ }
9558
+ if (options.nullable && source[sourceKey] === null) {
9559
+ target[targetKey] = null;
9560
+ return;
9561
+ }
9562
+ const value = source[sourceKey];
9563
+ if (typeof value !== "number" || Number.isNaN(value)) {
9564
+ errors.push(`${label3} must be a number.`);
9565
+ return;
9566
+ }
9567
+ if (options.integer && !Number.isInteger(value)) {
9568
+ errors.push(`${label3} must be an integer.`);
9569
+ return;
9570
+ }
9571
+ if (options.min !== void 0 && value < options.min) {
9572
+ errors.push(`${label3} must be >= ${options.min}.`);
9573
+ return;
9574
+ }
9575
+ if (options.max !== void 0 && value > options.max) {
9576
+ errors.push(`${label3} must be <= ${options.max}.`);
9577
+ return;
9578
+ }
9579
+ target[targetKey] = value;
9580
+ }
9581
+ function assignNullableInteger(source, sourceKey, target, targetKey, errors, label3, options = {}) {
9582
+ if (source[sourceKey] === void 0) {
9583
+ return;
9584
+ }
9585
+ if (source[sourceKey] === null) {
9586
+ target[targetKey] = null;
9587
+ return;
9588
+ }
9589
+ const value = source[sourceKey];
9590
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
9591
+ errors.push(`${label3} must be a non-negative integer or null.`);
9592
+ return;
9593
+ }
9594
+ if (options.max !== void 0 && value > options.max) {
9595
+ errors.push(`${label3} must be <= ${options.max}.`);
9596
+ return;
9597
+ }
9598
+ target[targetKey] = value;
9599
+ }
9600
+ function isMissingFileError(error) {
9601
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
9602
+ }
9603
+
9604
+ // src/commands/baseline.ts
9605
+ function registerBaselineCommand(program3) {
9606
+ const baseline = program3.command("baseline").description("Create, inspect, validate, or remove a CSS quality baseline");
9607
+ baseline.command("create").description("Create a baseline from the current CSS Health Score").argument(
9608
+ "[patterns...]",
9609
+ "CSS files or glob patterns (falls back to config.patterns)"
9610
+ ).option(
9611
+ "-o, --output <path>",
9612
+ `Baseline output path (default: ${DEFAULT_BASELINE_PATH})`
9613
+ ).option("--config <path>", "Project config path").option(
9614
+ "--threshold <number>",
9615
+ "Selector-count threshold stored with the baseline",
9616
+ parseNonNegativeInteger
9617
+ ).option("-j, --json", "Emit JSON instead of a human summary").action(async (patterns, options) => {
9618
+ const report = await runBaselineCreate(patterns ?? [], options);
9619
+ writeBaselineReport(report, options.json === true);
9620
+ process.exitCode = report.success ? 0 : 1;
9621
+ });
9622
+ baseline.command("show").description("Display baseline metadata and scores").option(
9623
+ "-b, --baseline <path>",
9624
+ `Baseline path (default: ${DEFAULT_BASELINE_PATH})`
9625
+ ).option("--config <path>", "Project config path").option("-j, --json", "Emit JSON instead of a human summary").action(async (options) => {
9626
+ const report = await runBaselineShow(options);
9627
+ writeBaselineReport(report, options.json === true);
9628
+ process.exitCode = report.success ? 0 : 1;
9629
+ });
9630
+ baseline.command("validate").description("Validate baseline schema and compatibility").option(
9631
+ "-b, --baseline <path>",
9632
+ `Baseline path (default: ${DEFAULT_BASELINE_PATH})`
9633
+ ).option("--config <path>", "Project config path").option("-j, --json", "Emit JSON instead of a human summary").action(async (options) => {
9634
+ const report = await runBaselineValidate(options);
9635
+ writeBaselineReport(report, options.json === true);
9636
+ process.exitCode = report.success ? 0 : 1;
9637
+ });
9638
+ baseline.command("remove").description("Delete the baseline file").option(
9639
+ "-b, --baseline <path>",
9640
+ `Baseline path (default: ${DEFAULT_BASELINE_PATH})`
9641
+ ).option("--config <path>", "Project config path").option("-j, --json", "Emit JSON instead of a human summary").action(async (options) => {
9642
+ const report = await runBaselineRemove(options);
9643
+ writeBaselineReport(report, options.json === true);
9644
+ process.exitCode = report.success ? 0 : 1;
9645
+ });
9646
+ }
9647
+ async function runBaselineCreate(patterns, options = {}) {
9648
+ const cwd = resolve3(options.cwd ?? process.cwd());
9649
+ const loaded = await loadProjectConfig(cwd, options.config);
9650
+ const errors = [...loaded.errors];
9651
+ const effectivePatterns = patterns.length > 0 ? patterns : loaded.config.patterns;
9652
+ const baselinePath = resolveBaselinePath(
9653
+ cwd,
9654
+ options.output ?? loaded.config.baseline
9655
+ );
9656
+ const snapshot = await createBaselineSnapshot({
9657
+ cwd,
9658
+ patterns: effectivePatterns,
9659
+ threshold: options.threshold ?? loaded.config.threshold
9660
+ });
9661
+ if (!snapshot.ok) {
9662
+ return {
9663
+ schemaVersion: 1,
9664
+ command: "baseline",
9665
+ action: "create",
9666
+ success: false,
9667
+ errors: [...errors, ...snapshot.errors],
9668
+ warnings: [],
9669
+ baselinePath: toDisplayPath(baselinePath, cwd),
9670
+ baseline: null
9671
+ };
9672
+ }
9673
+ const written = await writeBaselineFile(baselinePath, snapshot.json);
9674
+ if (!written.ok) {
9675
+ return {
9676
+ schemaVersion: 1,
9677
+ command: "baseline",
9678
+ action: "create",
9679
+ success: false,
9680
+ errors: [...errors, written.error],
9681
+ warnings: [],
9682
+ baselinePath: toDisplayPath(baselinePath, cwd),
9683
+ baseline: null
9684
+ };
9685
+ }
9686
+ return {
9687
+ schemaVersion: 1,
9688
+ command: "baseline",
9689
+ action: "create",
9690
+ success: errors.length === 0,
9691
+ errors,
9692
+ warnings: [],
9693
+ baselinePath: toDisplayPath(baselinePath, cwd),
9694
+ baseline: snapshot.document
9695
+ };
9696
+ }
9697
+ async function runBaselineShow(options = {}) {
9698
+ return readAndReport("show", options);
9699
+ }
9700
+ async function runBaselineValidate(options = {}) {
9701
+ const cwd = resolve3(options.cwd ?? process.cwd());
9702
+ const loaded = await loadProjectConfig(cwd, options.config);
9703
+ const errors = [...loaded.errors];
9704
+ const warnings = [];
9705
+ const baselinePath = resolveBaselinePath(
9706
+ cwd,
9707
+ options.baseline ?? loaded.config.baseline
9708
+ );
9709
+ const displayPath = toDisplayPath(baselinePath, cwd);
9710
+ const read = await readBaselineFile(baselinePath);
9711
+ if (!read.ok) {
9712
+ return {
9713
+ schemaVersion: 1,
9714
+ command: "baseline",
9715
+ action: "validate",
9716
+ success: false,
9717
+ errors: [...errors, read.error],
9718
+ warnings,
9719
+ baselinePath: displayPath,
9720
+ baseline: null
9721
+ };
9722
+ }
9723
+ try {
9724
+ const validation = validateBaselineJson(read.json);
9725
+ if (validation.status === "incompatible") {
9726
+ errors.push(validation.message ?? "Baseline is incompatible.");
9727
+ } else if (validation.status === "softWarning" && validation.message) {
9728
+ warnings.push(validation.message);
9729
+ }
9730
+ return {
9731
+ schemaVersion: 1,
9732
+ command: "baseline",
9733
+ action: "validate",
9734
+ success: errors.length === 0,
9735
+ errors,
9736
+ warnings,
9737
+ baselinePath: displayPath,
9738
+ baseline: JSON.parse(read.json)
9739
+ };
9740
+ } catch (error) {
9741
+ return {
9742
+ schemaVersion: 1,
9743
+ command: "baseline",
9744
+ action: "validate",
9745
+ success: false,
9746
+ errors: [
9747
+ ...errors,
9748
+ error instanceof Error ? error.message : String(error)
9749
+ ],
9750
+ warnings,
9751
+ baselinePath: displayPath,
9752
+ baseline: null
9753
+ };
9754
+ }
9755
+ }
9756
+ async function runBaselineRemove(options = {}) {
9757
+ const cwd = resolve3(options.cwd ?? process.cwd());
9758
+ const loaded = await loadProjectConfig(cwd, options.config);
9759
+ const errors = [...loaded.errors];
9760
+ const baselinePath = resolveBaselinePath(
9761
+ cwd,
9762
+ options.baseline ?? loaded.config.baseline
9763
+ );
9764
+ const displayPath = toDisplayPath(baselinePath, cwd);
9765
+ const removed = await removeBaselineFile(baselinePath);
9766
+ if (!removed.ok) {
9767
+ errors.push(removed.error);
9768
+ }
9769
+ return {
9770
+ schemaVersion: 1,
9771
+ command: "baseline",
9772
+ action: "remove",
9773
+ success: errors.length === 0,
9774
+ errors,
9775
+ warnings: [],
9776
+ baselinePath: displayPath,
9777
+ baseline: null
9778
+ };
9779
+ }
9780
+ async function readAndReport(action, options) {
9781
+ const cwd = resolve3(options.cwd ?? process.cwd());
9782
+ const loaded = await loadProjectConfig(cwd, options.config);
9783
+ const errors = [...loaded.errors];
9784
+ const baselinePath = resolveBaselinePath(
9785
+ cwd,
9786
+ options.baseline ?? loaded.config.baseline
9787
+ );
9788
+ const displayPath = toDisplayPath(baselinePath, cwd);
9789
+ const read = await readBaselineFile(baselinePath);
9790
+ if (!read.ok) {
9791
+ return {
9792
+ schemaVersion: 1,
9793
+ command: "baseline",
9794
+ action,
9795
+ success: false,
9796
+ errors: [...errors, read.error],
9797
+ warnings: [],
9798
+ baselinePath: displayPath,
9799
+ baseline: null
9800
+ };
9801
+ }
9802
+ try {
9803
+ return {
9804
+ schemaVersion: 1,
9805
+ command: "baseline",
9806
+ action,
9807
+ success: errors.length === 0,
9808
+ errors,
9809
+ warnings: [],
9810
+ baselinePath: displayPath,
9811
+ baseline: JSON.parse(read.json)
9812
+ };
9813
+ } catch (error) {
9814
+ return {
9815
+ schemaVersion: 1,
9816
+ command: "baseline",
9817
+ action,
9818
+ success: false,
9819
+ errors: [
9820
+ ...errors,
9821
+ `Corrupted baseline JSON at ${displayPath}: ${error instanceof Error ? error.message : String(error)}`
9822
+ ],
9823
+ warnings: [],
9824
+ baselinePath: displayPath,
9825
+ baseline: null
9826
+ };
9827
+ }
9828
+ }
9829
+ function writeBaselineReport(report, json) {
9830
+ if (json) {
9831
+ process.stdout.write(`${formatJsonReport(report)}
9832
+ `);
9833
+ return;
9834
+ }
9835
+ if (!report.success) {
9836
+ process.stdout.write(
9837
+ `css-doctor baseline ${report.action} failed:
9838
+ ${report.errors.map((error) => ` - ${error}`).join("\n")}
9839
+ `
9840
+ );
9841
+ return;
9842
+ }
9843
+ if (report.action === "remove") {
9844
+ process.stdout.write(`Removed baseline ${report.baselinePath}
9845
+ `);
9846
+ return;
9847
+ }
9848
+ if (report.action === "create") {
9849
+ process.stdout.write(
9850
+ `Created baseline ${report.baselinePath}` + (report.baseline ? ` \xB7 score ${report.baseline.health.overall}/100 \xB7 ${report.baseline.files.length} files
9851
+ ` : "\n")
9852
+ );
9853
+ } else if (report.baseline) {
9854
+ const health = report.baseline.health;
9855
+ process.stdout.write(
9856
+ [
9857
+ `Baseline ${report.baselinePath}`,
9858
+ ` schema ${report.baseline.schemaVersion} \xB7 tool ${report.baseline.toolVersion}`,
9859
+ ` created ${report.baseline.createdAt}`,
9860
+ ` score ${health.overall}/100 ${health.grade} (${health.status})`,
9861
+ ` ${health.files} files \xB7 ${health.rules} rules \xB7 ${health.sizeBytes} bytes`,
9862
+ ...report.warnings.map((warning) => ` warning: ${warning}`),
9863
+ ""
9864
+ ].join("\n")
9865
+ );
9866
+ }
9867
+ for (const warning of report.warnings) {
9868
+ if (report.action === "create") {
9869
+ process.stderr.write(`css-doctor: ${warning}
9870
+ `);
9871
+ }
9872
+ }
9873
+ }
9874
+ function parseNonNegativeInteger(value) {
9875
+ const parsed = Number(value);
9876
+ if (!Number.isInteger(parsed) || parsed < 0) {
9877
+ throw new InvalidArgumentError("must be a non-negative integer");
9878
+ }
9879
+ return parsed;
9880
+ }
9881
+
9882
+ // src/commands/check.ts
9883
+ init_esm_shims();
9884
+ import { createRequire as createRequire2 } from "module";
9885
+ import { performance } from "perf_hooks";
9886
+ import { resolve as resolve4 } from "path";
9887
+
9888
+ // src/formatters/health-terminal.ts
9889
+ init_esm_shims();
9890
+ var import_picocolors = __toESM(require_picocolors(), 1);
9891
+ var DEFAULT_TOP_ISSUES = 8;
9892
+ var BAR_WIDTH = 36;
9893
+ function formatHealthTerminalReport(report, options = {}) {
9894
+ const colors = import_picocolors.default.createColors(
9895
+ options.color ?? import_picocolors.default.isColorSupported
9896
+ );
9897
+ const columns = Math.max(
9898
+ 48,
9899
+ Math.min(options.columns ?? detectColumns(), 88)
9900
+ );
9901
+ const topIssues = options.topIssues ?? DEFAULT_TOP_ISSUES;
9902
+ const lines = [];
9903
+ const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
9904
+ lines.push("");
9905
+ lines.push(
9906
+ ` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad(
9907
+ columns - 22,
9908
+ ""
9909
+ )}${colors.dim("HEALTH REPORT")}`
9910
+ );
9911
+ lines.push("");
9912
+ lines.push(` ${colors.bold("Overall Health")}`);
9913
+ lines.push("");
9914
+ lines.push(
9915
+ ` ${scoreColor(report.overall, colors)(colors.bold(`${report.overall}/100`))} ${colors.bold(report.grade)} ${statusLabel(report.status, colors)}`
9916
+ );
9917
+ lines.push("");
9918
+ lines.push(` ${scoreBar(report.overall, colors)}`);
9919
+ lines.push("");
9920
+ lines.push(
9921
+ ` ${colors.dim(
9922
+ `Analyzed ${report.summary.files} file${report.summary.files === 1 ? "" : "s"} \xB7 ${formatNumber(report.summary.rules)} rules \xB7 ${formatNumber(report.summary.declarations)} declarations`
9923
+ )}`
9924
+ );
9925
+ lines.push(
9926
+ ` ${colors.dim(
9927
+ `Total size ${formatBytes(report.summary.sizeBytes)} \xB7 Analysis completed in ${formatDuration(report.summary.analysisMs)}`
9928
+ )}`
9929
+ );
9930
+ lines.push("");
9931
+ lines.push(rule);
9932
+ lines.push("");
9933
+ lines.push(` ${colors.bold("HEALTH BREAKDOWN")}`);
9934
+ lines.push("");
9935
+ for (const dimension of report.dimensions) {
9936
+ const label3 = dimensionLabel(dimension.id).padEnd(24);
9937
+ const scoreText = dimension.score === void 0 || dimension.score === null ? " n/a" : `${String(dimension.score).padStart(3, " ")}/100`;
9938
+ const bar = dimension.score === void 0 || dimension.score === null ? colors.dim("\u2591".repeat(20)) : miniBar(dimension.score, colors);
9939
+ lines.push(` ${label3}${scoreText} ${bar}`);
9940
+ }
9941
+ lines.push("");
9942
+ lines.push(rule);
9943
+ lines.push("");
9944
+ lines.push(` ${colors.bold("DIAGNOSTICS")}`);
9945
+ lines.push("");
9946
+ lines.push(
9947
+ ` ${colors.red("\u2716")} ${report.summary.errors} Error${report.summary.errors === 1 ? "" : "s"}`
9948
+ );
9949
+ lines.push(
9950
+ ` ${colors.yellow("\u25B2")} ${report.summary.warnings} Warning${report.summary.warnings === 1 ? "" : "s"}`
9951
+ );
9952
+ lines.push(
9953
+ ` ${colors.cyan("\u25CF")} ${report.summary.suggestions} Suggestion${report.summary.suggestions === 1 ? "" : "s"}`
9954
+ );
9955
+ if (report.diagnostics.length > 0) {
9956
+ lines.push("");
9957
+ lines.push(` ${colors.bold("TOP ISSUES")}`);
9958
+ lines.push("");
9959
+ const shown = report.diagnostics.slice(0, topIssues);
9960
+ for (const diagnostic of shown) {
9961
+ const marker = diagnostic.severity === "error" ? colors.red("\u2716") : diagnostic.severity === "warning" ? colors.yellow("\u25B2") : colors.cyan("\u25CF");
9962
+ lines.push(` ${marker} ${colors.bold(titleFromCode(diagnostic.code))}`);
9963
+ const where = [
9964
+ diagnostic.filePath,
9965
+ diagnostic.location ? `${diagnostic.location.line}:${diagnostic.location.column}` : void 0
9966
+ ].filter(Boolean).join(":");
9967
+ if (where) {
9968
+ lines.push(` ${colors.dim(where)}`);
9969
+ }
9970
+ if (diagnostic.selector) {
9971
+ lines.push(` ${colors.magenta(diagnostic.selector)}`);
9972
+ }
9973
+ lines.push(` ${colors.dim(diagnostic.remediation)}`);
9974
+ lines.push("");
9975
+ }
9976
+ if (report.diagnostics.length > shown.length) {
9977
+ lines.push(
9978
+ ` ${colors.dim(
9979
+ `\u2026 and ${report.diagnostics.length - shown.length} more (see --json for full list)`
9980
+ )}`
9981
+ );
9982
+ lines.push("");
9983
+ }
9984
+ }
9985
+ if (report.recommendations.length > 0) {
9986
+ lines.push(` ${colors.bold("RECOMMENDATIONS")}`);
9987
+ lines.push("");
9988
+ report.recommendations.forEach((recommendation, index) => {
9989
+ lines.push(` ${index + 1}. ${recommendation}`);
9990
+ });
9991
+ lines.push("");
9992
+ }
9993
+ lines.push(rule);
9994
+ lines.push("");
9995
+ lines.push(` ${finalStatusLine(report, colors)}`);
9996
+ lines.push("");
9997
+ lines.push(
9998
+ ` ${colors.dim("Run `css-doctor health --help` for more options.")}`
9999
+ );
10000
+ lines.push("");
10001
+ return lines.join("\n");
10002
+ }
10003
+ function formatCompactHealthSummary(report, options = {}) {
10004
+ const colors = import_picocolors.default.createColors(
10005
+ options.color ?? import_picocolors.default.isColorSupported
10006
+ );
10007
+ return `${colors.bold("Health")} ${scoreColor(
10008
+ report.overall,
10009
+ colors
10010
+ )(
10011
+ `${report.overall}/100`
10012
+ )} ${colors.bold(report.grade)} ${colors.dim(`(${report.status})`)} ${colors.dim("\xB7")} ${report.summary.errors} err ${colors.dim("\xB7")} ${report.summary.warnings} warn ${colors.dim("\xB7")} ${report.summary.suggestions} info`;
10013
+ }
10014
+ function finalStatusLine(report, colors) {
10015
+ if (report.overall >= 90) {
10016
+ return `${colors.green(colors.bold("HEALTHY"))} CSS quality is within recommended limits.`;
10017
+ }
10018
+ if (report.overall >= 70) {
10019
+ return `${colors.yellow(colors.bold("NEEDS ATTENTION"))} Address the top warnings before release.`;
10020
+ }
10021
+ return `${colors.red(colors.bold("CRITICAL"))} CSS quality is below recommended limits.`;
10022
+ }
10023
+ function scoreColor(score, colors) {
10024
+ if (score >= 90) {
10025
+ return colors.green;
10026
+ }
10027
+ if (score >= 70) {
10028
+ return colors.yellow;
10029
+ }
10030
+ return colors.red;
10031
+ }
10032
+ function statusLabel(status, colors) {
10033
+ const label3 = status.charAt(0).toUpperCase() + status.slice(1);
10034
+ if (status === "excellent" || status === "good") {
10035
+ return colors.green(label3);
10036
+ }
10037
+ if (status === "fair") {
10038
+ return colors.yellow(label3);
10039
+ }
10040
+ return colors.red(label3);
10041
+ }
10042
+ function scoreBar(score, colors) {
10043
+ const filled = Math.round(score / 100 * BAR_WIDTH);
10044
+ const empty = Math.max(0, BAR_WIDTH - filled);
10045
+ return `${scoreColor(score, colors)("\u2588".repeat(filled))}${colors.dim("\u2591".repeat(empty))}`;
10046
+ }
10047
+ function miniBar(score, colors) {
10048
+ const width = 20;
10049
+ const filled = Math.round(score / 100 * width);
10050
+ return `${scoreColor(score, colors)("\u2588".repeat(filled))}${colors.dim("\u2591".repeat(width - filled))}`;
10051
+ }
10052
+ function dimensionLabel(id) {
10053
+ switch (id) {
10054
+ case "selectorComplexity":
10055
+ return "Selector Complexity";
10056
+ case "duplicateRules":
10057
+ return "Duplicate Rules";
10058
+ case "cssSizeEfficiency":
10059
+ return "CSS Size Efficiency";
10060
+ case "deadCss":
10061
+ return "Dead CSS";
10062
+ case "variableConsistency":
10063
+ return "Variable Consistency";
10064
+ default:
10065
+ return id;
10066
+ }
10067
+ }
10068
+ function titleFromCode(code) {
10069
+ const leaf = code.split(".").pop() ?? code;
10070
+ return leaf.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
10071
+ }
10072
+ function detectColumns() {
10073
+ const columns = process.stdout.columns;
10074
+ return typeof columns === "number" && columns > 0 ? columns : 80;
10075
+ }
10076
+ function pad(width, text) {
10077
+ return " ".repeat(Math.max(1, width - text.length));
10078
+ }
10079
+ function formatBytes(bytes) {
10080
+ if (bytes < 1024) {
10081
+ return `${bytes} B`;
10082
+ }
10083
+ if (bytes < 1024 * 1024) {
10084
+ return `${(bytes / 1024).toFixed(1)} KB`;
10085
+ }
10086
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
10087
+ }
10088
+ function formatDuration(ms) {
10089
+ if (ms < 1) {
10090
+ return `${ms.toFixed(2)} ms`;
10091
+ }
10092
+ if (ms < 1e3) {
10093
+ return `${ms.toFixed(1)} ms`;
10094
+ }
10095
+ return `${(ms / 1e3).toFixed(2)} s`;
10096
+ }
10097
+ function formatNumber(value) {
10098
+ return new Intl.NumberFormat("en-US").format(value);
10099
+ }
10100
+
10101
+ // src/formatters/terminal.ts
10102
+ init_esm_shims();
10103
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
10104
+ var TOP_SELECTOR_LIMIT = 5;
10105
+ function terminalExitCode(report) {
10106
+ return report.success ? 0 : 1;
10107
+ }
10108
+ function formatTerminalReport(report, options = {}) {
10109
+ const colors = import_picocolors2.default.createColors(
10110
+ options.color ?? import_picocolors2.default.isColorSupported
10111
+ );
10112
+ const lines = [];
10113
+ const separator = colors.dim("\u2500".repeat(64));
10114
+ lines.push(
10115
+ `${colors.bold(colors.cyan("css-doctor"))} ${colors.dim(`v${report.version}`)} ${colors.dim("\xB7")} ${colors.dim("check")}`
10116
+ );
10117
+ lines.push(
10118
+ `${colors.dim("patterns")} ${report.patterns.length > 0 ? report.patterns.join(", ") : "(none)"}`
10119
+ );
10120
+ lines.push(separator);
10121
+ if (report.errors.length > 0) {
10122
+ lines.push(colors.red(colors.bold("ERRORS")));
10123
+ for (const error of report.errors) {
10124
+ lines.push(` ${colors.red("\u2716")} ${error}`);
10125
+ }
10126
+ lines.push("");
10127
+ }
10128
+ for (const file of report.files) {
10129
+ lines.push(...formatFile(file, colors));
10130
+ }
10131
+ if (report.files.length === 0 && report.errors.length === 0) {
10132
+ lines.push(colors.yellow("No files were analyzed."));
10133
+ lines.push("");
10134
+ }
10135
+ lines.push(separator);
10136
+ lines.push(...formatSummary(report, colors));
10137
+ return lines.join("\n");
10138
+ }
10139
+ function formatFile(file, colors) {
10140
+ const status = fileStatus(file);
10141
+ const lines = [];
10142
+ lines.push(
10143
+ `${statusBadge(status, colors)} ${colors.bold(file.path)} ${colors.dim(`\xB7 ${formatBytes2(file.sizeBytes)}`)}`
10144
+ );
10145
+ if (!file.ok) {
10146
+ lines.push(` ${colors.red(file.error ?? "Unknown error")}`);
10147
+ lines.push("");
9095
10148
  return lines;
9096
10149
  }
9097
- const { stats } = result;
10150
+ const result = file.result;
10151
+ if (!result) {
10152
+ lines.push(` ${colors.red("No analysis result was returned.")}`);
10153
+ lines.push("");
10154
+ return lines;
10155
+ }
10156
+ const { stats } = result;
10157
+ lines.push(
10158
+ ` ${colors.dim("selectors")} ${stats.selectorCount} ${colors.dim("\xB7")} ${colors.dim("rules")} ${stats.ruleCount} ${colors.dim("\xB7")} ${colors.dim("declarations")} ${stats.declarationCount + stats.importantDeclarationCount}`
10159
+ );
10160
+ lines.push(
10161
+ ` ${colors.dim("analysis")} ${formatDuration2(file.analysisMs)} ${colors.dim("\xB7")} ${colors.dim("read")} ${formatDuration2(file.readMs)} ${colors.dim("\xB7")} ${colors.dim("total")} ${formatDuration2(file.totalMs)}`
10162
+ );
10163
+ if (file.issues.length > 0) {
10164
+ lines.push("");
10165
+ lines.push(
10166
+ ` ${colors.yellow("\u26A0")} ${colors.bold("Diagnostics")} ${colors.dim(`(${file.issues.length})`)}`
10167
+ );
10168
+ for (const issue of file.issues) {
10169
+ lines.push(` ${formatIssue(issue, colors)}`);
10170
+ }
10171
+ }
10172
+ if (result.selectors.length > 0) {
10173
+ lines.push("");
10174
+ lines.push(...formatTopSelectors(result, colors));
10175
+ }
10176
+ lines.push("");
10177
+ return lines;
10178
+ }
10179
+ function formatTopSelectors(result, colors) {
10180
+ const top = topSelectors(result.selectors, TOP_SELECTOR_LIMIT);
10181
+ const total = result.selectors.length;
10182
+ const heading = total > top.length ? `Top selectors (${top.length} of ${total})` : `Top selectors (${total})`;
10183
+ const lines = [` ${colors.bold(heading)}`];
10184
+ top.forEach((selector, index) => {
10185
+ const frequency = selector.count > 1 ? ` ${colors.yellow(`\xD7${selector.count}`)}` : "";
10186
+ lines.push(
10187
+ ` ${colors.dim(`${String(index + 1).padStart(2, " ")}.`)} ${selector.raw}${frequency} ${colors.dim(`(${selector.specificity})`)}`
10188
+ );
10189
+ });
10190
+ return lines;
10191
+ }
10192
+ function topSelectors(selectors, limit) {
10193
+ const entries = /* @__PURE__ */ new Map();
10194
+ selectors.forEach((selector, index) => {
10195
+ const existing = entries.get(selector.raw);
10196
+ if (existing) {
10197
+ existing.count += 1;
10198
+ return;
10199
+ }
10200
+ entries.set(selector.raw, {
10201
+ raw: selector.raw,
10202
+ count: 1,
10203
+ firstIndex: index,
10204
+ specificity: `${selector.specificity.ids},${selector.specificity.classes},${selector.specificity.types}`
10205
+ });
10206
+ });
10207
+ return [...entries.values()].sort(
10208
+ (left, right) => right.count - left.count || left.firstIndex - right.firstIndex
10209
+ ).slice(0, limit);
10210
+ }
10211
+ function formatIssue(issue, colors) {
10212
+ const location = issue.location ? `${issue.location.line}:${issue.location.column}` : "\u2014";
10213
+ const tag = issue.kind === "syntax" ? colors.yellow("syntax") : issue.kind === "threshold" ? colors.yellow("threshold") : issue.kind === "empty-rule" ? colors.magenta("dead code") : colors.magenta("duplicate");
10214
+ return `${colors.dim(location.padEnd(7))} ${tag} ${issue.message}`;
10215
+ }
10216
+ function formatSummary(report, colors) {
10217
+ const { summary } = report;
10218
+ const status = reportStatus(report);
10219
+ const lines = [];
10220
+ lines.push(
10221
+ `${statusBadge(status, colors)} ${summary.files} file${summary.files === 1 ? "" : "s"} ${colors.dim("\xB7")} ${formatBytes2(summary.totalSizeBytes)} ${colors.dim("\xB7")} ${summary.selectors} selectors ${colors.dim("\xB7")} ${formatDuration2(summary.totalMs)}`
10222
+ );
10223
+ lines.push(
10224
+ ` ${colors.dim("analyzed")} ${summary.analyzed} ${colors.dim("\xB7")} ${colors.dim("failed")} ${summary.failed} ${colors.dim("\xB7")} ${colors.dim("warnings")} ${summary.warnings} ${colors.dim("\xB7")} ${colors.dim("hints")} ${summary.hints}`
10225
+ );
10226
+ lines.push(
10227
+ ` ${colors.dim("rules")} ${summary.rules} ${colors.dim("\xB7")} ${colors.dim("declarations")} ${summary.declarations} ${colors.dim("\xB7")} ${colors.dim("analysis")} ${formatDuration2(summary.analysisMs)}`
10228
+ );
10229
+ if (report.threshold !== null) {
10230
+ lines.push(` ${colors.dim("selector threshold")} ${report.threshold}`);
10231
+ }
10232
+ return lines;
10233
+ }
10234
+ function fileStatus(file) {
10235
+ if (!file.ok) {
10236
+ return "ERROR";
10237
+ }
10238
+ return file.issues.length > 0 ? "WARNING" : "SUCCESS";
10239
+ }
10240
+ function reportStatus(report) {
10241
+ if (!report.success || report.errors.length > 0 || report.summary.failed > 0) {
10242
+ return "ERROR";
10243
+ }
10244
+ return report.summary.issues > 0 ? "WARNING" : "SUCCESS";
10245
+ }
10246
+ function statusBadge(status, colors) {
10247
+ switch (status) {
10248
+ case "SUCCESS":
10249
+ return colors.bgGreen(colors.black(" SUCCESS "));
10250
+ case "WARNING":
10251
+ return colors.bgYellow(colors.black(" WARNING "));
10252
+ case "ERROR":
10253
+ return colors.bgRed(colors.white(" ERROR "));
10254
+ }
10255
+ }
10256
+ function formatBytes2(bytes) {
10257
+ if (bytes < 1024) {
10258
+ return `${bytes} B`;
10259
+ }
10260
+ if (bytes < 1024 * 1024) {
10261
+ return `${(bytes / 1024).toFixed(1)} KB`;
10262
+ }
10263
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
10264
+ }
10265
+ function formatDuration2(milliseconds) {
10266
+ if (!Number.isFinite(milliseconds)) {
10267
+ return "\u2014";
10268
+ }
10269
+ return milliseconds < 1 ? `${milliseconds.toFixed(2)} ms` : `${milliseconds.toFixed(1)} ms`;
10270
+ }
10271
+
10272
+ // src/commands/check.ts
10273
+ var require3 = createRequire2(import.meta.url);
10274
+ var cachedBinding2;
10275
+ function loadBinding2() {
10276
+ if (cachedBinding2) {
10277
+ return cachedBinding2;
10278
+ }
10279
+ try {
10280
+ cachedBinding2 = require3("@zealakit/css-doctor-binding");
10281
+ return cachedBinding2;
10282
+ } catch (error) {
10283
+ throw new Error(
10284
+ "Unable to load @zealakit/css-doctor-binding. Build it first with `pnpm --filter @zealakit/css-doctor-binding build`.\n" + describeError(error)
10285
+ );
10286
+ }
10287
+ }
10288
+ function registerCheckCommand(program3) {
10289
+ program3.command("check").description("Analyze CSS files and report diagnostics").argument(
10290
+ "[patterns...]",
10291
+ "CSS files or glob patterns (quote globs to let css-doctor expand them)"
10292
+ ).option("-j, --json", "Output raw JSON for piping and CI/CD").option(
10293
+ "--threshold <number>",
10294
+ "Warn when a file contains more than <number> selectors",
10295
+ parseThreshold
10296
+ ).option(
10297
+ "--fail-under <number>",
10298
+ "Exit with code 1 when overall health score is below <number> (0-100)",
10299
+ parseScore
10300
+ ).action(async (patterns, options) => {
10301
+ const report = await runCheck(patterns ?? [], {
10302
+ threshold: options.threshold,
10303
+ failUnder: options.failUnder
10304
+ });
10305
+ if (options.json === true) {
10306
+ process.stdout.write(`${formatJsonReport(report)}
10307
+ `);
10308
+ } else {
10309
+ let output = formatTerminalReport(report);
10310
+ if (report.health) {
10311
+ output += `
10312
+ ${formatCompactHealthSummary(report.health)}
10313
+ `;
10314
+ }
10315
+ process.stdout.write(`${output}
10316
+ `);
10317
+ }
10318
+ process.exitCode = terminalExitCode(report);
10319
+ });
10320
+ }
10321
+ function parseThreshold(value) {
10322
+ const parsed = Number(value);
10323
+ if (!Number.isInteger(parsed) || parsed < 0) {
10324
+ throw new InvalidArgumentError("threshold must be a non-negative integer");
10325
+ }
10326
+ return parsed;
10327
+ }
10328
+ function parseScore(value) {
10329
+ const parsed = Number(value);
10330
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
10331
+ throw new InvalidArgumentError("must be an integer between 0 and 100");
10332
+ }
10333
+ return parsed;
10334
+ }
10335
+ async function runCheck(patterns, options = {}) {
10336
+ const startedAt = performance.now();
10337
+ const cwd = resolve4(options.cwd ?? process.cwd());
10338
+ const threshold = options.threshold ?? null;
10339
+ const failUnder = options.failUnder ?? null;
10340
+ const errors = [];
10341
+ const results = [];
10342
+ let files = [];
10343
+ if (threshold !== null && (!Number.isInteger(threshold) || threshold < 0)) {
10344
+ errors.push("Threshold must be a non-negative integer.");
10345
+ }
10346
+ if (failUnder !== null && (failUnder < 0 || failUnder > 100)) {
10347
+ errors.push("--fail-under must be an integer between 0 and 100.");
10348
+ }
10349
+ if (patterns.length === 0) {
10350
+ errors.push(
10351
+ 'No CSS files or glob patterns were provided. Example: css-doctor check "src/**/*.css"'
10352
+ );
10353
+ } else if (!await isDirectory(cwd)) {
10354
+ errors.push(
10355
+ `Working directory does not exist or is not a directory: ${cwd}`
10356
+ );
10357
+ } else {
10358
+ try {
10359
+ files = await resolveCssFiles(patterns, cwd);
10360
+ if (files.length === 0) {
10361
+ errors.push(`No CSS files matched: ${patterns.join(", ")}`);
10362
+ }
10363
+ } catch (error) {
10364
+ errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
10365
+ }
10366
+ }
10367
+ let health = null;
10368
+ if (errors.length === 0) {
10369
+ try {
10370
+ const binding = loadBinding2();
10371
+ const analyze = binding.analyzeCss;
10372
+ for (const file of files) {
10373
+ results.push(await checkFile(file, cwd, threshold, analyze));
10374
+ }
10375
+ const healthFiles = [];
10376
+ for (const file of files) {
10377
+ const read = await readCssFile(file);
10378
+ if (!read.ok) {
10379
+ continue;
10380
+ }
10381
+ healthFiles.push({
10382
+ cssContent: read.content,
10383
+ filePath: toDisplayPath(file, cwd),
10384
+ sizeBytes: read.sizeBytes
10385
+ });
10386
+ }
10387
+ if (healthFiles.length > 0) {
10388
+ health = binding.evaluateCssHealthBatch({
10389
+ files: healthFiles,
10390
+ thresholds: threshold === null ? void 0 : {
10391
+ manySelectors: threshold
10392
+ }
10393
+ });
10394
+ }
10395
+ } catch (error) {
10396
+ errors.push(describeError(error));
10397
+ }
10398
+ }
10399
+ const issues = results.flatMap((file) => file.issues);
10400
+ const warnings = issues.filter((issue) => issue.kind === "syntax").length;
10401
+ const analyzed = results.filter((file) => file.ok).length;
10402
+ const failed = results.length - analyzed;
10403
+ const summary = {
10404
+ files: results.length,
10405
+ analyzed,
10406
+ failed,
10407
+ filesWithIssues: results.filter((file) => file.issues.length > 0).length,
10408
+ totalSizeBytes: results.reduce((total, file) => total + file.sizeBytes, 0),
10409
+ rules: sum(results, (file) => file.result?.stats.ruleCount ?? 0),
10410
+ selectors: sum(results, (file) => file.result?.stats.selectorCount ?? 0),
10411
+ declarations: sum(
10412
+ results,
10413
+ (file) => (file.result?.stats.declarationCount ?? 0) + (file.result?.stats.importantDeclarationCount ?? 0)
10414
+ ),
10415
+ warnings,
10416
+ hints: issues.length - warnings,
10417
+ issues: issues.length,
10418
+ analysisMs: roundMs(
10419
+ results.reduce((total, file) => total + file.analysisMs, 0)
10420
+ ),
10421
+ totalMs: elapsedMs(startedAt)
10422
+ };
10423
+ const belowFloor = failUnder !== null && health !== null && health.overall < failUnder ? [
10424
+ `Overall health score ${health.overall} is below --fail-under ${failUnder}.`
10425
+ ] : [];
10426
+ return {
10427
+ schemaVersion: 1,
10428
+ command: "check",
10429
+ version: package_default.version,
10430
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10431
+ cwd,
10432
+ patterns,
10433
+ threshold,
10434
+ failUnder,
10435
+ success: errors.length === 0 && failed === 0 && belowFloor.length === 0,
10436
+ errors: [...errors, ...belowFloor],
10437
+ files: results,
10438
+ summary,
10439
+ health
10440
+ };
10441
+ }
10442
+ async function checkFile(filePath, cwd, threshold, analyze) {
10443
+ const path2 = toDisplayPath(filePath, cwd);
10444
+ const readStartedAt = performance.now();
10445
+ const read = await readCssFile(filePath);
10446
+ const readMs = elapsedMs(readStartedAt);
10447
+ if (!read.ok) {
10448
+ return {
10449
+ path: path2,
10450
+ absolutePath: filePath,
10451
+ sizeBytes: 0,
10452
+ ok: false,
10453
+ totalMs: readMs,
10454
+ readMs,
10455
+ analysisMs: 0,
10456
+ issues: [],
10457
+ error: read.error
10458
+ };
10459
+ }
10460
+ const analysisStartedAt = performance.now();
10461
+ try {
10462
+ const result = analyze(read.content);
10463
+ const analysisMs = elapsedMs(analysisStartedAt);
10464
+ return {
10465
+ path: path2,
10466
+ absolutePath: filePath,
10467
+ sizeBytes: read.sizeBytes,
10468
+ ok: true,
10469
+ totalMs: roundMs(readMs + analysisMs),
10470
+ readMs,
10471
+ analysisMs,
10472
+ issues: collectIssues(result, threshold),
10473
+ result
10474
+ };
10475
+ } catch (error) {
10476
+ const analysisMs = elapsedMs(analysisStartedAt);
10477
+ return {
10478
+ path: path2,
10479
+ absolutePath: filePath,
10480
+ sizeBytes: read.sizeBytes,
10481
+ ok: false,
10482
+ totalMs: roundMs(readMs + analysisMs),
10483
+ readMs,
10484
+ analysisMs,
10485
+ issues: [],
10486
+ error: `Failed to analyze file: ${describeError(error)}`
10487
+ };
10488
+ }
10489
+ }
10490
+ function collectIssues(result, threshold) {
10491
+ const issues = [];
10492
+ for (const warning of result.warnings) {
10493
+ issues.push({
10494
+ kind: "syntax",
10495
+ severity: "warning",
10496
+ message: warning.message,
10497
+ ...warning.location ? { location: warning.location } : {}
10498
+ });
10499
+ }
10500
+ const parentRuleIndices = new Set(
10501
+ result.rules.map((rule) => rule.parentRuleIndex).filter((index) => index !== void 0)
10502
+ );
10503
+ for (const rule of result.rules) {
10504
+ const hasNoDeclarations = rule.declarationCount === 0 && rule.importantDeclarationCount === 0;
10505
+ const hasNoNestedRules = !parentRuleIndices.has(rule.index);
10506
+ if (hasNoDeclarations && hasNoNestedRules) {
10507
+ const selectorText = rule.selectorIndices.map((index) => result.selectors[index]?.raw).filter((value) => value !== void 0).join(", ");
10508
+ issues.push({
10509
+ kind: "empty-rule",
10510
+ severity: "warning",
10511
+ message: selectorText.length > 0 ? `Rule "${selectorText}" has no declarations and no nested rules (dead code candidate).` : "Rule has no declarations and no nested rules (dead code candidate).",
10512
+ location: rule.location,
10513
+ ...selectorText.length > 0 ? { selector: selectorText } : {}
10514
+ });
10515
+ }
10516
+ }
10517
+ const seenSelectors = /* @__PURE__ */ new Map();
10518
+ for (const selector of result.selectors) {
10519
+ const first = seenSelectors.get(selector.raw);
10520
+ if (!first) {
10521
+ seenSelectors.set(selector.raw, selector);
10522
+ continue;
10523
+ }
10524
+ const firstRule = result.rules[first.ruleIndex];
10525
+ const currentRule = result.rules[selector.ruleIndex];
10526
+ issues.push({
10527
+ kind: "duplicate-selector",
10528
+ severity: "warning",
10529
+ message: `Selector "${selector.raw}" is defined in multiple rules` + (firstRule ? ` (first at line ${firstRule.location.line})` : "") + "; verify whether this is intentional.",
10530
+ ...currentRule ? { location: currentRule.location } : {},
10531
+ selector: selector.raw
10532
+ });
10533
+ }
10534
+ if (threshold !== null && result.selectors.length > threshold) {
10535
+ issues.push({
10536
+ kind: "threshold",
10537
+ severity: "warning",
10538
+ message: `Selector threshold exceeded: ${result.selectors.length} selectors found (threshold: ${threshold}).`
10539
+ });
10540
+ }
10541
+ return issues.sort(compareIssues);
10542
+ }
10543
+ function compareIssues(left, right) {
10544
+ const leftLine = left.location?.line ?? Number.MAX_SAFE_INTEGER;
10545
+ const rightLine = right.location?.line ?? Number.MAX_SAFE_INTEGER;
10546
+ const lineDifference = leftLine - rightLine;
10547
+ if (lineDifference !== 0) {
10548
+ return lineDifference;
10549
+ }
10550
+ const columnDifference = (left.location?.column ?? 0) - (right.location?.column ?? 0);
10551
+ if (columnDifference !== 0) {
10552
+ return columnDifference;
10553
+ }
10554
+ return left.kind.localeCompare(right.kind);
10555
+ }
10556
+ function sum(files, select) {
10557
+ return files.reduce((total, file) => total + select(file), 0);
10558
+ }
10559
+ function elapsedMs(startedAt) {
10560
+ return roundMs(performance.now() - startedAt);
10561
+ }
10562
+ function roundMs(value) {
10563
+ return Math.round(value * 1e3) / 1e3;
10564
+ }
10565
+
10566
+ // src/commands/diff.ts
10567
+ init_esm_shims();
10568
+ import { resolve as resolve5 } from "path";
10569
+
10570
+ // src/formatters/regression-terminal.ts
10571
+ init_esm_shims();
10572
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
10573
+ function formatRegressionTerminalReport(report, options = {}) {
10574
+ const colors = import_picocolors3.default.createColors(
10575
+ options.color ?? import_picocolors3.default.isColorSupported
10576
+ );
10577
+ const columns = Math.max(
10578
+ 48,
10579
+ Math.min(options.columns ?? detectColumns2(), 88)
10580
+ );
10581
+ const verbose = options.verbose === true;
10582
+ const lines = [];
10583
+ const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
10584
+ lines.push("");
10585
+ lines.push(
10586
+ ` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad2(
10587
+ columns - 28,
10588
+ ""
10589
+ )}${colors.dim("REGRESSION REPORT")}`
10590
+ );
10591
+ lines.push("");
10592
+ lines.push(` ${colors.bold("Overall Health")}`);
10593
+ lines.push("");
10594
+ lines.push(
10595
+ ` ${scoreColor2(
10596
+ report.current.score,
10597
+ colors
10598
+ )(
10599
+ colors.bold(`${report.current.score}/100`)
10600
+ )} ${colors.bold(report.current.grade)} ${statusWord(
10601
+ report.current.status,
10602
+ colors
10603
+ )}`
10604
+ );
10605
+ lines.push("");
10606
+ lines.push(` ${label("Baseline").padEnd(14)}${report.baseline.score}/100`);
10607
+ lines.push(` ${label("Current").padEnd(14)}${report.current.score}/100`);
10608
+ lines.push(
10609
+ ` ${label("Change").padEnd(14)}${formatSigned(
10610
+ report.changes.score,
10611
+ "points",
10612
+ colors
10613
+ )}`
10614
+ );
10615
+ lines.push("");
10616
+ lines.push(` ${statusBanner(report.status, colors)}`);
10617
+ lines.push("");
10618
+ lines.push(
10619
+ ` ${colors.dim(
10620
+ `Analyzed ${report.current.files} files \xB7 ${formatNumber2(report.current.rules)} rules \xB7 ${formatNumber2(report.current.declarations)} declarations`
10621
+ )}`
10622
+ );
10623
+ if (report.warnings.length > 0) {
10624
+ lines.push("");
10625
+ for (const warning of report.warnings) {
10626
+ lines.push(` ${colors.yellow("!")} ${colors.dim(warning)}`);
10627
+ }
10628
+ }
10629
+ lines.push("");
10630
+ lines.push(rule);
10631
+ lines.push("");
10632
+ lines.push(` ${colors.bold("DIMENSION COMPARISON")}`);
10633
+ lines.push("");
9098
10634
  lines.push(
9099
- ` ${colors.dim("selectors")} ${stats.selectorCount} ${colors.dim("\xB7")} ${colors.dim("rules")} ${stats.ruleCount} ${colors.dim("\xB7")} ${colors.dim("declarations")} ${stats.declarationCount + stats.importantDeclarationCount}`
10635
+ ` ${"Dimension".padEnd(26)}${"Baseline".padStart(8)} ${"Current".padStart(8)} ${"Change".padStart(7)}`
9100
10636
  );
10637
+ lines.push("");
10638
+ for (const dimension of report.changes.dimensions) {
10639
+ lines.push(` ${formatDimensionRow(dimension, colors)}`);
10640
+ }
10641
+ lines.push("");
10642
+ lines.push(rule);
10643
+ lines.push("");
10644
+ lines.push(` ${colors.bold("CSS SIZE ANALYSIS")}`);
10645
+ lines.push("");
9101
10646
  lines.push(
9102
- ` ${colors.dim("analysis")} ${formatDuration(file.analysisMs)} ${colors.dim("\xB7")} ${colors.dim("read")} ${formatDuration(file.readMs)} ${colors.dim("\xB7")} ${colors.dim("total")} ${formatDuration(file.totalMs)}`
10647
+ ` ${formatMetricLine(
10648
+ "Total size",
10649
+ formatBytes3(report.changes.cssSize.baseline),
10650
+ formatBytes3(report.changes.cssSize.current),
10651
+ report.changes.cssSize.changePercent,
10652
+ colors
10653
+ )}`
9103
10654
  );
9104
- if (file.issues.length > 0) {
10655
+ lines.push(
10656
+ ` ${formatMetricLine(
10657
+ "Rules",
10658
+ formatNumber2(report.changes.rules.baseline),
10659
+ formatNumber2(report.changes.rules.current),
10660
+ report.changes.rules.changePercent,
10661
+ colors
10662
+ )}`
10663
+ );
10664
+ lines.push(
10665
+ ` ${formatMetricLine(
10666
+ "Declarations",
10667
+ formatNumber2(report.changes.declarations.baseline),
10668
+ formatNumber2(report.changes.declarations.current),
10669
+ report.changes.declarations.changePercent,
10670
+ colors
10671
+ )}`
10672
+ );
10673
+ const largest = [...report.files.modified].filter((file) => (file.sizeChangePercent ?? 0) > 0).sort(
10674
+ (left, right) => (right.sizeChangePercent ?? 0) - (left.sizeChangePercent ?? 0)
10675
+ ).slice(0, verbose ? 10 : 3);
10676
+ if (largest.length > 0) {
10677
+ lines.push("");
10678
+ lines.push(` ${colors.bold("LARGEST INCREASES")}`);
10679
+ lines.push("");
10680
+ for (const file of largest) {
10681
+ lines.push(` ${file.path}`);
10682
+ lines.push(
10683
+ ` ${formatBytes3(file.baselineSizeBytes ?? 0)} \u2192 ${formatBytes3(
10684
+ file.currentSizeBytes ?? 0
10685
+ )} ${formatPercent(file.sizeChangePercent, colors)}`
10686
+ );
10687
+ lines.push("");
10688
+ }
10689
+ }
10690
+ lines.push(rule);
10691
+ lines.push("");
10692
+ lines.push(` ${colors.bold("DIAGNOSTIC CHANGES")}`);
10693
+ lines.push("");
10694
+ lines.push(
10695
+ ` ${colors.red("+")} ${report.diagnostics.new.length} New issues`
10696
+ );
10697
+ lines.push(
10698
+ ` ${colors.green("-")} ${report.diagnostics.resolved.length} Resolved issues`
10699
+ );
10700
+ lines.push(
10701
+ ` ${colors.dim("=")} ${report.diagnostics.persistent.length} Persistent issues`
10702
+ );
10703
+ const newIssues = report.diagnostics.new.slice(0, verbose ? 20 : 5);
10704
+ if (newIssues.length > 0) {
10705
+ lines.push("");
10706
+ lines.push(` ${colors.bold("NEW ISSUES")}`);
10707
+ lines.push("");
10708
+ for (const diagnostic of newIssues) {
10709
+ const marker = diagnostic.severity === "error" ? colors.red("\u2716") : colors.yellow("\u25B2");
10710
+ lines.push(` ${marker} ${colors.bold(titleFromCode2(diagnostic.code))}`);
10711
+ const where = [diagnostic.filePath, diagnostic.selector].filter(Boolean).join(" \xB7 ");
10712
+ if (where) {
10713
+ lines.push(` ${colors.dim(where)}`);
10714
+ }
10715
+ }
10716
+ if (report.diagnostics.new.length > newIssues.length) {
10717
+ lines.push("");
10718
+ lines.push(
10719
+ ` ${colors.dim(
10720
+ `\u2026 and ${report.diagnostics.new.length - newIssues.length} more`
10721
+ )}`
10722
+ );
10723
+ }
10724
+ }
10725
+ if (report.files.added.length > 0 || report.files.removed.length > 0 || report.files.regressed.length > 0) {
10726
+ lines.push("");
10727
+ lines.push(rule);
10728
+ lines.push("");
10729
+ lines.push(` ${colors.bold("FILE CHANGES")}`);
9105
10730
  lines.push("");
9106
10731
  lines.push(
9107
- ` ${colors.yellow("\u26A0")} ${colors.bold("Diagnostics")} ${colors.dim(`(${file.issues.length})`)}`
10732
+ ` + ${report.files.added.length} added \xB7 - ${report.files.removed.length} removed \xB7 ~ ${report.files.modified.length} modified`
9108
10733
  );
9109
- for (const issue of file.issues) {
9110
- lines.push(` ${formatIssue(issue, colors)}`);
10734
+ if (verbose) {
10735
+ for (const file of report.files.added.slice(0, 10)) {
10736
+ lines.push(` ${colors.green("+")} ${file.path}`);
10737
+ }
10738
+ for (const file of report.files.removed.slice(0, 10)) {
10739
+ lines.push(` ${colors.red("-")} ${file.path}`);
10740
+ }
10741
+ for (const file of report.files.regressed.slice(0, 10)) {
10742
+ lines.push(` ${colors.yellow("~")} ${file.path} (size growth)`);
10743
+ }
10744
+ }
10745
+ }
10746
+ if (report.qualityGates && report.qualityGates.results.length > 0) {
10747
+ lines.push("");
10748
+ lines.push(rule);
10749
+ lines.push("");
10750
+ lines.push(...formatQualityGates(report.qualityGates, colors));
10751
+ }
10752
+ lines.push("");
10753
+ lines.push(rule);
10754
+ lines.push("");
10755
+ lines.push(` ${finalStatusLine2(report.status, colors)}`);
10756
+ lines.push("");
10757
+ return lines.join("\n");
10758
+ }
10759
+ function formatQualityGates(gates, colors) {
10760
+ const lines = [` ${colors.bold("QUALITY GATES")}`, ""];
10761
+ for (const result of gates.results) {
10762
+ const marker = result.passed ? colors.green("\u2713") : colors.red("\u2717");
10763
+ lines.push(
10764
+ ` ${marker} ${result.label.padEnd(26)} ${colors.dim(result.detail)}`
10765
+ );
10766
+ }
10767
+ lines.push("");
10768
+ lines.push(
10769
+ ` RESULT: ${gates.passed ? colors.green(colors.bold("PASSED")) : colors.red(colors.bold("FAILED"))}`
10770
+ );
10771
+ if (!gates.passed && gates.failureSummary) {
10772
+ lines.push("");
10773
+ lines.push(` ${gates.failureSummary}`);
10774
+ }
10775
+ return lines;
10776
+ }
10777
+ function formatDimensionRow(dimension, colors) {
10778
+ const labelText = dimensionLabel2(dimension.id).padEnd(26);
10779
+ const baseline = dimension.baseline === null || dimension.baseline === void 0 ? "n/a".padStart(8) : String(dimension.baseline).padStart(8);
10780
+ const current = dimension.current === null || dimension.current === void 0 ? "n/a".padStart(8) : String(dimension.current).padStart(8);
10781
+ const changeText = dimension.change === null || dimension.change === void 0 ? "n/a" : dimension.change > 0 ? `+${dimension.change}` : `${dimension.change}`;
10782
+ const meaningful = dimension.change !== null && dimension.change !== void 0 && Math.abs(dimension.change) > 1;
10783
+ const changeColored = dimension.change === null || dimension.change === void 0 ? colors.dim(changeText.padStart(7)) : dimension.change > 0 ? colors.green(changeText.padStart(7)) : dimension.change < 0 ? colors.red(changeText.padStart(7)) : colors.dim(changeText.padStart(7));
10784
+ const row = `${labelText}${baseline} ${current} ${changeColored}`;
10785
+ return meaningful ? row : colors.dim(
10786
+ `${labelText}${baseline} ${current} ${changeText.padStart(7)}`
10787
+ );
10788
+ }
10789
+ function formatMetricLine(labelText, baseline, current, percent, colors) {
10790
+ return `${labelText.padEnd(14)}${baseline} \u2192 ${current} ${formatPercent(
10791
+ percent,
10792
+ colors
10793
+ )}`;
10794
+ }
10795
+ function statusBanner(status, colors) {
10796
+ const text = `STATUS: ${status.toUpperCase()}`;
10797
+ switch (status) {
10798
+ case "healthy":
10799
+ case "improved":
10800
+ return colors.green(colors.bold(text));
10801
+ case "mixed":
10802
+ case "inconclusive":
10803
+ return colors.yellow(colors.bold(text));
10804
+ case "regressed":
10805
+ return colors.red(colors.bold(text));
10806
+ default:
10807
+ return colors.bold(text);
10808
+ }
10809
+ }
10810
+ function finalStatusLine2(status, colors) {
10811
+ switch (status) {
10812
+ case "healthy":
10813
+ return `${colors.green(colors.bold("HEALTHY"))} No meaningful CSS regression detected.`;
10814
+ case "improved":
10815
+ return `${colors.green(colors.bold("IMPROVED"))} CSS quality improved relative to baseline.`;
10816
+ case "mixed":
10817
+ return `${colors.yellow(colors.bold("MIXED"))} Improvements and regressions coexist.`;
10818
+ case "inconclusive":
10819
+ return `${colors.yellow(colors.bold("INCONCLUSIVE"))} Insufficient comparable data.`;
10820
+ case "regressed":
10821
+ return `${colors.red(colors.bold("REGRESSED"))} CSS quality deteriorated relative to baseline.`;
10822
+ default:
10823
+ return status;
10824
+ }
10825
+ }
10826
+ function scoreColor2(score, colors) {
10827
+ if (score >= 90) {
10828
+ return colors.green;
10829
+ }
10830
+ if (score >= 70) {
10831
+ return colors.yellow;
10832
+ }
10833
+ return colors.red;
10834
+ }
10835
+ function statusWord(status, colors) {
10836
+ const labelText = status.charAt(0).toUpperCase() + status.slice(1);
10837
+ if (status === "excellent" || status === "good") {
10838
+ return colors.green(labelText);
10839
+ }
10840
+ if (status === "fair") {
10841
+ return colors.yellow(labelText);
10842
+ }
10843
+ return colors.red(labelText);
10844
+ }
10845
+ function formatSigned(value, unit, colors) {
10846
+ return `${formatSignedNumber(value, colors)} ${unit}`;
10847
+ }
10848
+ function formatSignedNumber(value, colors) {
10849
+ const text = value > 0 ? `+${value}` : `${value}`;
10850
+ if (value > 0) {
10851
+ return colors.green(text);
10852
+ }
10853
+ if (value < 0) {
10854
+ return colors.red(text);
10855
+ }
10856
+ return colors.dim(text);
10857
+ }
10858
+ function formatPercent(value, colors) {
10859
+ if (value === null || value === void 0) {
10860
+ return colors.dim("n/a");
10861
+ }
10862
+ const text = `${value > 0 ? "+" : ""}${value.toFixed(1)}%`;
10863
+ if (value > 0) {
10864
+ return colors.red(text);
10865
+ }
10866
+ if (value < 0) {
10867
+ return colors.green(text);
10868
+ }
10869
+ return colors.dim(text);
10870
+ }
10871
+ function dimensionLabel2(id) {
10872
+ switch (id) {
10873
+ case "selectorComplexity":
10874
+ return "Selector Complexity";
10875
+ case "duplicateRules":
10876
+ return "Duplicate Rules";
10877
+ case "cssSizeEfficiency":
10878
+ return "CSS Size Efficiency";
10879
+ case "deadCss":
10880
+ return "Dead CSS";
10881
+ case "variableConsistency":
10882
+ return "Variable Consistency";
10883
+ default:
10884
+ return id;
10885
+ }
10886
+ }
10887
+ function titleFromCode2(code) {
10888
+ const leaf = code.split(".").pop() ?? code;
10889
+ return leaf.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
10890
+ }
10891
+ function detectColumns2() {
10892
+ const columns = process.stdout.columns;
10893
+ return typeof columns === "number" && columns > 0 ? columns : 80;
10894
+ }
10895
+ function pad2(width, text) {
10896
+ return " ".repeat(Math.max(1, width - text.length));
10897
+ }
10898
+ function label(text) {
10899
+ return text;
10900
+ }
10901
+ function formatBytes3(bytes) {
10902
+ if (bytes < 1024) {
10903
+ return `${bytes} B`;
10904
+ }
10905
+ if (bytes < 1024 * 1024) {
10906
+ return `${(bytes / 1024).toFixed(1)} KB`;
10907
+ }
10908
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
10909
+ }
10910
+ function formatNumber2(value) {
10911
+ return new Intl.NumberFormat("en-US").format(value);
10912
+ }
10913
+
10914
+ // src/commands/diff.ts
10915
+ function registerDiffCommand(program3) {
10916
+ program3.command("diff").description("Compare current CSS quality against a committed baseline").argument(
10917
+ "[patterns...]",
10918
+ "CSS files or glob patterns (falls back to config.patterns or baseline patterns)"
10919
+ ).option(
10920
+ "-b, --baseline <path>",
10921
+ `Baseline path (default: ${DEFAULT_BASELINE_PATH})`
10922
+ ).option("--config <path>", "Project config path").option(
10923
+ "--threshold <number>",
10924
+ "Selector-count threshold for current analysis",
10925
+ parseNonNegativeInteger2
10926
+ ).option("--fail-on-regression", "Exit 1 when status is regressed or mixed").option("--fail-on-new-errors", "Exit 1 when new error diagnostics appear").option(
10927
+ "--fail-on-new-diagnostics",
10928
+ "Exit 1 when any new diagnostics appear"
10929
+ ).option(
10930
+ "--max-size-increase <value>",
10931
+ "Fail when CSS size grows by more than this percent (e.g. 10 or 10%)",
10932
+ parsePercent
10933
+ ).option(
10934
+ "--max-new-errors <number>",
10935
+ "Fail when new error count exceeds this value",
10936
+ parseNonNegativeInteger2
10937
+ ).option(
10938
+ "--max-new-warnings <number>",
10939
+ "Fail when new warning count exceeds this value",
10940
+ parseNonNegativeInteger2
10941
+ ).option(
10942
+ "--min-score <number>",
10943
+ "Fail when current overall score is below this value",
10944
+ parseScore2
10945
+ ).option(
10946
+ "--score-tolerance <number>",
10947
+ "Ignore overall/dimension score deltas within this absolute tolerance",
10948
+ parseNonNegativeInteger2
10949
+ ).option("-v, --verbose", "Show detailed file and diagnostic lists").option("-j, --json", "Output raw JSON for piping and CI/CD").action(async (patterns, options) => {
10950
+ const report = await runDiff(patterns ?? [], options);
10951
+ if (options.json === true) {
10952
+ process.stdout.write(`${formatJsonReport(report)}
10953
+ `);
10954
+ } else if (report.regression) {
10955
+ process.stdout.write(
10956
+ `${formatRegressionTerminalReport(report.regression, {
10957
+ verbose: options.verbose === true
10958
+ })}
10959
+ `
10960
+ );
10961
+ if (report.errors.length > 0) {
10962
+ process.stderr.write(
10963
+ `css-doctor: ${report.errors.join("\ncss-doctor: ")}
10964
+ `
10965
+ );
10966
+ }
10967
+ } else {
10968
+ process.stdout.write(
10969
+ `css-doctor diff failed:
10970
+ ${report.errors.map((error) => ` - ${error}`).join("\n")}
10971
+ `
10972
+ );
10973
+ }
10974
+ process.exitCode = report.success ? 0 : 1;
10975
+ });
10976
+ }
10977
+ async function runDiff(patterns, options = {}) {
10978
+ const cwd = resolve5(options.cwd ?? process.cwd());
10979
+ const loaded = await loadProjectConfig(cwd, options.config);
10980
+ const errors = [...loaded.errors];
10981
+ const warnings = [];
10982
+ const baselinePath = resolveBaselinePath(
10983
+ cwd,
10984
+ options.baseline ?? loaded.config.baseline
10985
+ );
10986
+ const displayPath = toDisplayPath(baselinePath, cwd);
10987
+ const stored = await readBaselineFile(baselinePath);
10988
+ if (!stored.ok) {
10989
+ return {
10990
+ schemaVersion: 1,
10991
+ command: "diff",
10992
+ success: false,
10993
+ errors: [...errors, stored.error],
10994
+ warnings,
10995
+ baselinePath: displayPath,
10996
+ regression: null
10997
+ };
10998
+ }
10999
+ try {
11000
+ const validation = validateBaselineJson(stored.json);
11001
+ if (validation.status === "incompatible") {
11002
+ return {
11003
+ schemaVersion: 1,
11004
+ command: "diff",
11005
+ success: false,
11006
+ errors: [
11007
+ ...errors,
11008
+ validation.message ?? "Baseline is incompatible with this tool version."
11009
+ ],
11010
+ warnings,
11011
+ baselinePath: displayPath,
11012
+ regression: null
11013
+ };
11014
+ }
11015
+ if (validation.status === "softWarning" && validation.message) {
11016
+ warnings.push(validation.message);
11017
+ }
11018
+ } catch (error) {
11019
+ return {
11020
+ schemaVersion: 1,
11021
+ command: "diff",
11022
+ success: false,
11023
+ errors: [
11024
+ ...errors,
11025
+ error instanceof Error ? error.message : String(error)
11026
+ ],
11027
+ warnings,
11028
+ baselinePath: displayPath,
11029
+ regression: null
11030
+ };
11031
+ }
11032
+ let baselinePatterns = [];
11033
+ try {
11034
+ const parsed = JSON.parse(stored.json);
11035
+ baselinePatterns = parsed.config?.patterns ?? [];
11036
+ } catch {
11037
+ baselinePatterns = [];
11038
+ }
11039
+ const effectivePatterns = patterns.length > 0 ? patterns : options.patterns && options.patterns.length > 0 ? options.patterns : loaded.config.patterns.length > 0 ? loaded.config.patterns : baselinePatterns;
11040
+ const current = await createBaselineSnapshot({
11041
+ cwd,
11042
+ patterns: effectivePatterns,
11043
+ threshold: options.threshold ?? loaded.config.threshold
11044
+ });
11045
+ if (!current.ok) {
11046
+ return {
11047
+ schemaVersion: 1,
11048
+ command: "diff",
11049
+ success: false,
11050
+ errors: [...errors, ...current.errors],
11051
+ warnings,
11052
+ baselinePath: displayPath,
11053
+ regression: null
11054
+ };
11055
+ }
11056
+ try {
11057
+ const binding = loadBinding();
11058
+ const compareOptions = {
11059
+ scoreTolerance: options.scoreTolerance ?? loaded.config.regression.scoreTolerance,
11060
+ sizeAbsoluteToleranceBytes: loaded.config.regression.sizeAbsoluteToleranceBytes,
11061
+ sizeIncreasePercent: loaded.config.regression.sizeIncreasePercent
11062
+ };
11063
+ const gates = buildGateConfig(options, loaded.config);
11064
+ const regressionJson = binding.compareBaselines(
11065
+ stored.json,
11066
+ current.json,
11067
+ compareOptions,
11068
+ gates
11069
+ );
11070
+ const regression = JSON.parse(regressionJson);
11071
+ warnings.push(...regression.warnings ?? []);
11072
+ const gateFailed = regression.qualityGates ? regression.qualityGates.passed === false : false;
11073
+ const setupOk = errors.length === 0;
11074
+ return {
11075
+ schemaVersion: 1,
11076
+ command: "diff",
11077
+ success: setupOk && !gateFailed,
11078
+ errors: [
11079
+ ...errors,
11080
+ ...gateFailed && regression.qualityGates?.failureSummary ? [`Quality gates failed: ${regression.qualityGates.failureSummary}`] : []
11081
+ ],
11082
+ warnings,
11083
+ baselinePath: displayPath,
11084
+ regression
11085
+ };
11086
+ } catch (error) {
11087
+ return {
11088
+ schemaVersion: 1,
11089
+ command: "diff",
11090
+ success: false,
11091
+ errors: [
11092
+ ...errors,
11093
+ error instanceof Error ? error.message : String(error)
11094
+ ],
11095
+ warnings,
11096
+ baselinePath: displayPath,
11097
+ regression: null
11098
+ };
11099
+ }
11100
+ }
11101
+ function buildGateConfig(options, config) {
11102
+ const maxSizeIncrease = options.maxSizeIncrease !== void 0 ? typeof options.maxSizeIncrease === "number" ? options.maxSizeIncrease : parsePercent(String(options.maxSizeIncrease)) : config.qualityGates.maxSizeIncreasePercent ?? config.regression.sizeIncreasePercent;
11103
+ const minScore = options.minScore ?? config.qualityGates.minScore ?? void 0;
11104
+ const maxNewErrors = options.maxNewErrors ?? config.qualityGates.maxNewErrors ?? config.regression.maxNewErrors ?? void 0;
11105
+ const maxNewWarnings = options.maxNewWarnings ?? config.qualityGates.maxNewWarnings ?? config.regression.maxNewWarnings ?? void 0;
11106
+ const failOnRegression = options.failOnRegression === true || config.qualityGates.failOnRegression === true;
11107
+ const failOnNewErrors = options.failOnNewErrors === true || config.qualityGates.failOnNewErrors === true;
11108
+ const failOnNewDiagnostics = options.failOnNewDiagnostics === true || config.qualityGates.failOnNewDiagnostics === true;
11109
+ const gates = {};
11110
+ if (minScore !== null && minScore !== void 0) {
11111
+ gates.minScore = minScore;
11112
+ }
11113
+ if (options.maxSizeIncrease !== void 0 || config.qualityGates.maxSizeIncreasePercent !== null || failOnRegression) {
11114
+ if (options.maxSizeIncrease !== void 0 || config.qualityGates.maxSizeIncreasePercent !== null) {
11115
+ gates.maxSizeIncreasePercent = maxSizeIncrease ?? void 0;
11116
+ }
11117
+ }
11118
+ if (maxNewErrors !== null && maxNewErrors !== void 0) {
11119
+ gates.maxNewErrors = maxNewErrors;
11120
+ }
11121
+ if (maxNewWarnings !== null && maxNewWarnings !== void 0) {
11122
+ gates.maxNewWarnings = maxNewWarnings;
11123
+ }
11124
+ if (failOnRegression) {
11125
+ gates.failOnRegression = true;
11126
+ }
11127
+ if (failOnNewErrors) {
11128
+ gates.failOnNewErrors = true;
11129
+ }
11130
+ if (failOnNewDiagnostics) {
11131
+ gates.failOnNewDiagnostics = true;
11132
+ }
11133
+ return gates;
11134
+ }
11135
+ function parseNonNegativeInteger2(value) {
11136
+ const parsed = Number(value);
11137
+ if (!Number.isInteger(parsed) || parsed < 0) {
11138
+ throw new InvalidArgumentError("must be a non-negative integer");
11139
+ }
11140
+ return parsed;
11141
+ }
11142
+ function parseScore2(value) {
11143
+ const parsed = Number(value);
11144
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
11145
+ throw new InvalidArgumentError("must be an integer between 0 and 100");
11146
+ }
11147
+ return parsed;
11148
+ }
11149
+ function parsePercent(value) {
11150
+ const trimmed = value.trim().endsWith("%") ? value.trim().slice(0, -1) : value.trim();
11151
+ const parsed = Number(trimmed);
11152
+ if (!Number.isFinite(parsed) || parsed < 0) {
11153
+ throw new InvalidArgumentError("must be a non-negative percent (e.g. 10%)");
11154
+ }
11155
+ return parsed;
11156
+ }
11157
+
11158
+ // src/commands/fix.ts
11159
+ init_esm_shims();
11160
+ import { stat as stat2 } from "fs/promises";
11161
+ import { resolve as resolve6 } from "path";
11162
+
11163
+ // src/formatters/optimization-terminal.ts
11164
+ init_esm_shims();
11165
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
11166
+ function formatOptimizationTerminalReport(report, options = {}) {
11167
+ const colors = import_picocolors4.default.createColors(
11168
+ options.color ?? import_picocolors4.default.isColorSupported
11169
+ );
11170
+ const columns = Math.max(
11171
+ 48,
11172
+ Math.min(options.columns ?? detectColumns3(), 88)
11173
+ );
11174
+ const verbose = options.verbose === true;
11175
+ const lines = [];
11176
+ const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
11177
+ lines.push("");
11178
+ lines.push(
11179
+ ` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad3(
11180
+ columns - 30,
11181
+ ""
11182
+ )}${colors.dim(
11183
+ options.dryRun === true ? "OPTIMIZATION PREVIEW" : "OPTIMIZATION REPORT"
11184
+ )}`
11185
+ );
11186
+ lines.push("");
11187
+ lines.push(
11188
+ ` ${colors.bold(
11189
+ options.dryRun === true ? "Optimization preview" : "Optimization completed"
11190
+ )}`
11191
+ );
11192
+ lines.push("");
11193
+ lines.push(
11194
+ ` ${label2("Original size").padEnd(20)}${formatBytes4(report.input.bytes)}`
11195
+ );
11196
+ lines.push(
11197
+ ` ${label2("Emitted total").padEnd(20)}${formatBytes4(report.output.bytes)}`
11198
+ );
11199
+ const eliminated = report.impact.bytesEliminated;
11200
+ const elimPct = report.impact.eliminationPercent;
11201
+ lines.push(
11202
+ ` ${label2("Eliminated").padEnd(20)}${formatSignedBytes(eliminated, colors)}${elimPct === null || elimPct === void 0 ? "" : colors.dim(
11203
+ ` (${elimPct > 0 ? "+" : ""}${elimPct.toFixed(1)}% vs reference)`
11204
+ )}`
11205
+ );
11206
+ if (report.impact.redistributedBytes !== 0) {
11207
+ lines.push(
11208
+ ` ${colors.dim(
11209
+ `Redistributed (split duplication): ${formatSignedBytes(report.impact.redistributedBytes, colors)}`
11210
+ )}`
11211
+ );
11212
+ }
11213
+ lines.push("");
11214
+ lines.push(` ${label2("Files analyzed").padEnd(20)}${report.input.files}`);
11215
+ lines.push(
11216
+ ` ${label2("Output chunks").padEnd(20)}${report.impact.chunksCreated}`
11217
+ );
11218
+ lines.push(
11219
+ ` ${label2("Selectors removed").padEnd(20)}${report.impact.selectorsRemoved}`
11220
+ );
11221
+ lines.push(
11222
+ ` ${label2("Variables chunks").padEnd(20)}${report.impact.variablesChunks}`
11223
+ );
11224
+ lines.push("");
11225
+ lines.push(
11226
+ ` ${colors.dim(`Processing time ${formatDuration3(report.analysisMs)}`)}`
11227
+ );
11228
+ lines.push("");
11229
+ lines.push(rule);
11230
+ lines.push("");
11231
+ lines.push(` ${colors.bold("OPTIMIZATION BREAKDOWN")}`);
11232
+ lines.push("");
11233
+ lines.push(
11234
+ ` ${statusLine("Minification", report.operations.minified, colors)}`
11235
+ );
11236
+ lines.push(
11237
+ ` ${statusLine("Page coverage splitting", report.operations.pageCoverageSplit, colors)}`
11238
+ );
11239
+ lines.push(
11240
+ ` ${statusLine("Size budget splitting", report.operations.sizeBudgetSplit, colors)}`
11241
+ );
11242
+ lines.push(
11243
+ ` ${statusLine("Variable isolation", report.operations.variablesIsolated, colors)}`
11244
+ );
11245
+ lines.push(
11246
+ ` ${statusLine("Dead CSS elimination", report.operations.deadCodeEliminated, colors)}`
11247
+ );
11248
+ if (report.chunks.length > 0) {
11249
+ lines.push("");
11250
+ lines.push(` ${colors.bold("OUTPUT DISTRIBUTION")}`);
11251
+ lines.push("");
11252
+ const shown = report.chunks.slice(0, verbose ? 20 : 8);
11253
+ for (const chunk of shown) {
11254
+ const mark = chunk.oversized ? colors.yellow("\u25B2") : colors.dim("\xB7");
11255
+ lines.push(
11256
+ ` ${mark} ${chunk.name.padEnd(24)} ${formatBytes4(chunk.sizeBytes)}`
11257
+ );
11258
+ }
11259
+ if (report.chunks.length > shown.length) {
11260
+ lines.push(
11261
+ ` ${colors.dim(`\u2026 and ${report.chunks.length - shown.length} more chunks`)}`
11262
+ );
11263
+ }
11264
+ }
11265
+ const decisions = report.decisions.slice(0, verbose ? 30 : 8);
11266
+ if (decisions.length > 0) {
11267
+ lines.push("");
11268
+ lines.push(rule);
11269
+ lines.push("");
11270
+ lines.push(` ${colors.bold("OPTIMIZATION DECISIONS")}`);
11271
+ lines.push("");
11272
+ for (const decision of decisions) {
11273
+ lines.push(...formatDecision(decision, colors));
11274
+ }
11275
+ if (report.decisions.length > decisions.length) {
11276
+ lines.push(
11277
+ ` ${colors.dim(
11278
+ `\u2026 and ${report.decisions.length - decisions.length} more (use --verbose or --json)`
11279
+ )}`
11280
+ );
11281
+ }
11282
+ }
11283
+ if (report.warnings.length > 0) {
11284
+ lines.push("");
11285
+ for (const warning of report.warnings) {
11286
+ lines.push(` ${colors.yellow("!")} ${warning}`);
9111
11287
  }
9112
11288
  }
9113
- if (result.selectors.length > 0) {
11289
+ if (options.dryRun === true) {
9114
11290
  lines.push("");
9115
- lines.push(...formatTopSelectors(result, colors));
9116
- }
9117
- lines.push("");
9118
- return lines;
9119
- }
9120
- function formatTopSelectors(result, colors) {
9121
- const top = topSelectors(result.selectors, TOP_SELECTOR_LIMIT);
9122
- const total = result.selectors.length;
9123
- const heading = total > top.length ? `Top selectors (${top.length} of ${total})` : `Top selectors (${total})`;
9124
- const lines = [` ${colors.bold(heading)}`];
9125
- top.forEach((selector, index) => {
9126
- const frequency = selector.count > 1 ? ` ${colors.yellow(`\xD7${selector.count}`)}` : "";
11291
+ lines.push(rule);
11292
+ lines.push("");
11293
+ lines.push(` ${colors.bold("No files were modified.")}`);
9127
11294
  lines.push(
9128
- ` ${colors.dim(`${String(index + 1).padStart(2, " ")}.`)} ${selector.raw}${frequency} ${colors.dim(`(${selector.specificity})`)}`
11295
+ ` ${colors.dim("Run without --dry-run and with --out-dir to emit chunks.")}`
9129
11296
  );
9130
- });
9131
- return lines;
9132
- }
9133
- function topSelectors(selectors, limit) {
9134
- const entries = /* @__PURE__ */ new Map();
9135
- selectors.forEach((selector, index) => {
9136
- const existing = entries.get(selector.raw);
9137
- if (existing) {
9138
- existing.count += 1;
9139
- return;
9140
- }
9141
- entries.set(selector.raw, {
9142
- raw: selector.raw,
9143
- count: 1,
9144
- firstIndex: index,
9145
- specificity: `${selector.specificity.ids},${selector.specificity.classes},${selector.specificity.types}`
9146
- });
9147
- });
9148
- return [...entries.values()].sort(
9149
- (left, right) => right.count - left.count || left.firstIndex - right.firstIndex
9150
- ).slice(0, limit);
9151
- }
9152
- function formatIssue(issue, colors) {
9153
- const location = issue.location ? `${issue.location.line}:${issue.location.column}` : "\u2014";
9154
- const tag = issue.kind === "syntax" ? colors.yellow("syntax") : issue.kind === "threshold" ? colors.yellow("threshold") : issue.kind === "empty-rule" ? colors.magenta("dead code") : colors.magenta("duplicate");
9155
- return `${colors.dim(location.padEnd(7))} ${tag} ${issue.message}`;
11297
+ }
11298
+ lines.push("");
11299
+ return lines.join("\n");
9156
11300
  }
9157
- function formatSummary(report, colors) {
9158
- const { summary } = report;
9159
- const status = reportStatus(report);
11301
+ function formatFixTerminalReport(results, options = {}) {
11302
+ const colors = import_picocolors4.default.createColors(
11303
+ options.color ?? import_picocolors4.default.isColorSupported
11304
+ );
11305
+ const columns = Math.max(
11306
+ 48,
11307
+ Math.min(options.columns ?? detectColumns3(), 88)
11308
+ );
9160
11309
  const lines = [];
11310
+ const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
11311
+ const safe = results.reduce((sum2, result) => sum2 + result.plan.safeCount, 0);
11312
+ const review = results.reduce(
11313
+ (sum2, result) => sum2 + result.plan.reviewCount,
11314
+ 0
11315
+ );
11316
+ const unsafe = results.reduce(
11317
+ (sum2, result) => sum2 + result.plan.unsafeCount,
11318
+ 0
11319
+ );
11320
+ const applied = results.reduce(
11321
+ (sum2, result) => sum2 + result.plan.items.filter((item) => item.applied).length,
11322
+ 0
11323
+ );
11324
+ const changedFiles = results.filter((result) => result.changed).length;
11325
+ const issues = results.reduce(
11326
+ (sum2, result) => sum2 + result.plan.items.length,
11327
+ 0
11328
+ );
11329
+ lines.push("");
9161
11330
  lines.push(
9162
- `${statusBadge(status, colors)} ${summary.files} file${summary.files === 1 ? "" : "s"} ${colors.dim("\xB7")} ${formatBytes(summary.totalSizeBytes)} ${colors.dim("\xB7")} ${summary.selectors} selectors ${colors.dim("\xB7")} ${formatDuration(summary.totalMs)}`
11331
+ ` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad3(
11332
+ columns - 22,
11333
+ ""
11334
+ )}${colors.dim("FIX REPORT")}`
9163
11335
  );
11336
+ lines.push("");
11337
+ lines.push(` ${colors.bold("Fix analysis completed")}`);
11338
+ lines.push("");
11339
+ lines.push(` ${issues} issues detected`);
11340
+ lines.push(` ${safe} safe fixes available`);
11341
+ lines.push(` ${review} review-required fixes`);
11342
+ lines.push(` ${unsafe} unsafe fixes skipped by default`);
11343
+ lines.push("");
11344
+ lines.push(` Applied:`);
9164
11345
  lines.push(
9165
- ` ${colors.dim("analyzed")} ${summary.analyzed} ${colors.dim("\xB7")} ${colors.dim("failed")} ${summary.failed} ${colors.dim("\xB7")} ${colors.dim("warnings")} ${summary.warnings} ${colors.dim("\xB7")} ${colors.dim("hints")} ${summary.hints}`
11346
+ ` ${colors.green("\u2713")} ${applied} fix occurrence(s)${options.dryRun === true ? colors.dim(" (dry-run)") : ""}`
9166
11347
  );
11348
+ lines.push("");
11349
+ lines.push(` Skipped:`);
11350
+ lines.push(` ${colors.yellow("\u25B2")} ${review} fixes require review`);
11351
+ lines.push(` ${colors.red("\u2715")} ${unsafe} fixes may change CSS behavior`);
11352
+ lines.push("");
9167
11353
  lines.push(
9168
- ` ${colors.dim("rules")} ${summary.rules} ${colors.dim("\xB7")} ${colors.dim("declarations")} ${summary.declarations} ${colors.dim("\xB7")} ${colors.dim("analysis")} ${formatDuration(summary.analysisMs)}`
11354
+ ` Files ${options.dryRun === true ? "that would change" : "modified"}: ${changedFiles}`
9169
11355
  );
9170
- if (report.threshold !== null) {
9171
- lines.push(` ${colors.dim("selector threshold")} ${report.threshold}`);
11356
+ if (options.verbose === true) {
11357
+ lines.push("");
11358
+ lines.push(rule);
11359
+ lines.push("");
11360
+ for (const result of results) {
11361
+ if (!result.filePath) {
11362
+ continue;
11363
+ }
11364
+ lines.push(` ${result.filePath}`);
11365
+ for (const item of result.plan.items.slice(0, 10)) {
11366
+ const marker = item.applied ? colors.green("\u2713") : colors.dim("\xB7");
11367
+ lines.push(` ${marker} ${item.rule} \u2014 ${item.message}`);
11368
+ }
11369
+ }
9172
11370
  }
9173
- return lines;
11371
+ if (options.dryRun === true) {
11372
+ lines.push("");
11373
+ lines.push(` ${colors.bold("No files were modified.")}`);
11374
+ lines.push(
11375
+ ` ${colors.dim("Re-run without --dry-run to apply Safe fixes.")}`
11376
+ );
11377
+ }
11378
+ lines.push("");
11379
+ return lines.join("\n");
9174
11380
  }
9175
- function fileStatus(file) {
9176
- if (!file.ok) {
9177
- return "ERROR";
11381
+ function formatUnifiedDiff(filePath, before, after, options = {}) {
11382
+ if (before === after) {
11383
+ return "";
9178
11384
  }
9179
- return file.issues.length > 0 ? "WARNING" : "SUCCESS";
11385
+ const maxLines = options.maxLines ?? 80;
11386
+ const beforeLines = before.split("\n");
11387
+ const afterLines = after.split("\n");
11388
+ const lines = [`--- a/${filePath}`, `+++ b/${filePath}`, `@@ preview @@`];
11389
+ for (const line of beforeLines.slice(0, Math.floor(maxLines / 2))) {
11390
+ lines.push(`-${line}`);
11391
+ }
11392
+ for (const line of afterLines.slice(0, Math.floor(maxLines / 2))) {
11393
+ lines.push(`+${line}`);
11394
+ }
11395
+ if (beforeLines.length + afterLines.length > maxLines) {
11396
+ lines.push(`\\ No newline / truncated for preview`);
11397
+ }
11398
+ return `${lines.join("\n")}
11399
+ `;
9180
11400
  }
9181
- function reportStatus(report) {
9182
- if (!report.success || report.errors.length > 0 || report.summary.failed > 0) {
9183
- return "ERROR";
11401
+ function formatDecision(decision, colors) {
11402
+ const marker = decision.type === "oversized-chunk" || decision.type === "retain-uncertain" ? colors.yellow("\u25B2") : decision.changed ? colors.green("\u2713") : colors.cyan("\u25CF");
11403
+ const lines = [` ${marker} ${colors.bold(decision.title)}`];
11404
+ if (decision.source.selector) {
11405
+ lines.push(` ${colors.magenta(decision.source.selector)}`);
9184
11406
  }
9185
- return report.summary.issues > 0 ? "WARNING" : "SUCCESS";
11407
+ lines.push(` ${colors.dim(decision.explanation)}`);
11408
+ return [...lines, ""];
9186
11409
  }
9187
- function statusBadge(status, colors) {
9188
- switch (status) {
9189
- case "SUCCESS":
9190
- return colors.bgGreen(colors.black(" SUCCESS "));
9191
- case "WARNING":
9192
- return colors.bgYellow(colors.black(" WARNING "));
9193
- case "ERROR":
9194
- return colors.bgRed(colors.white(" ERROR "));
11410
+ function statusLine(labelText, enabled, colors) {
11411
+ return `${labelText.padEnd(24)} ${enabled ? colors.green("Completed") : colors.dim("Skipped")}`;
11412
+ }
11413
+ function formatSignedBytes(value, colors) {
11414
+ const abs = formatBytes4(Math.abs(value));
11415
+ if (value > 0) {
11416
+ return colors.green(`+${abs}`);
9195
11417
  }
11418
+ if (value < 0) {
11419
+ return colors.red(`-${abs}`);
11420
+ }
11421
+ return colors.dim(abs);
9196
11422
  }
9197
- function formatBytes(bytes) {
11423
+ function label2(text) {
11424
+ return text;
11425
+ }
11426
+ function detectColumns3() {
11427
+ const columns = process.stdout.columns;
11428
+ return typeof columns === "number" && columns > 0 ? columns : 80;
11429
+ }
11430
+ function pad3(width, text) {
11431
+ return " ".repeat(Math.max(1, width - text.length));
11432
+ }
11433
+ function formatBytes4(bytes) {
9198
11434
  if (bytes < 1024) {
9199
11435
  return `${bytes} B`;
9200
11436
  }
9201
11437
  if (bytes < 1024 * 1024) {
9202
11438
  return `${(bytes / 1024).toFixed(1)} KB`;
9203
11439
  }
9204
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
11440
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
9205
11441
  }
9206
- function formatDuration(milliseconds) {
9207
- if (!Number.isFinite(milliseconds)) {
9208
- return "\u2014";
11442
+ function formatDuration3(ms) {
11443
+ if (ms < 1) {
11444
+ return `${ms.toFixed(2)} ms`;
9209
11445
  }
9210
- return milliseconds < 1 ? `${milliseconds.toFixed(2)} ms` : `${milliseconds.toFixed(1)} ms`;
11446
+ if (ms < 1e3) {
11447
+ return `${ms.toFixed(1)} ms`;
11448
+ }
11449
+ return `${(ms / 1e3).toFixed(2)} s`;
9211
11450
  }
9212
11451
 
9213
- // src/utils/file.ts
11452
+ // src/utils/atomic-write.ts
9214
11453
  init_esm_shims();
9215
- var import_fast_glob = __toESM(require_out4(), 1);
9216
- import { readFile, stat } from "fs/promises";
9217
- import { isAbsolute, relative, resolve, sep } from "path";
9218
- var DEFAULT_IGNORES = ["**/node_modules/**", "**/.git/**"];
9219
- async function isFile(filePath) {
11454
+ import { randomBytes } from "crypto";
11455
+ import {
11456
+ chmod,
11457
+ copyFile,
11458
+ mkdir as mkdir2,
11459
+ rename,
11460
+ unlink as unlink2,
11461
+ writeFile as writeFile2
11462
+ } from "fs/promises";
11463
+ import { dirname as dirname2, join } from "path";
11464
+ async function atomicWriteFile(absolutePath, content, options = {}) {
11465
+ const directory = dirname2(absolutePath);
11466
+ const tempPath = join(
11467
+ directory,
11468
+ `.css-doctor-${randomBytes(8).toString("hex")}.tmp`
11469
+ );
9220
11470
  try {
9221
- return (await stat(filePath)).isFile();
9222
- } catch {
9223
- return false;
11471
+ await mkdir2(directory, { recursive: true });
11472
+ if (options.backup === true) {
11473
+ try {
11474
+ await copyFile(absolutePath, `${absolutePath}.bak`);
11475
+ } catch {
11476
+ }
11477
+ }
11478
+ await writeFile2(tempPath, content, "utf8");
11479
+ if (options.mode !== void 0) {
11480
+ await chmod(tempPath, options.mode);
11481
+ }
11482
+ await rename(tempPath, absolutePath);
11483
+ return { ok: true };
11484
+ } catch (error) {
11485
+ try {
11486
+ await unlink2(tempPath);
11487
+ } catch {
11488
+ }
11489
+ return {
11490
+ ok: false,
11491
+ error: `Failed to write ${absolutePath}: ${describeError(error)}`
11492
+ };
9224
11493
  }
9225
11494
  }
9226
- async function isDirectory(directoryPath) {
11495
+
11496
+ // src/commands/fix.ts
11497
+ function registerFixCommand(program3) {
11498
+ program3.command("fix").description("Plan or apply Safe CSS autofixes").argument(
11499
+ "[patterns...]",
11500
+ "CSS files or glob patterns (required unless --list)"
11501
+ ).option("--list", "List available autofix rules and exit").option("--dry-run", "Preview fixes without writing files").option("--write", "Apply Safe fixes to source files").option(
11502
+ "--rule <id>",
11503
+ "Only consider this fix rule id (repeatable)",
11504
+ collectRule,
11505
+ []
11506
+ ).option("--backup", "Write `.bak` copies before modifying files").option("--diff", "Show unified diffs for changed files").option("-v, --verbose", "List per-file fix items").option("-j, --json", "Emit JSON").action(
11507
+ async (patterns, options) => {
11508
+ if (options.list === true) {
11509
+ const report2 = await runFixList();
11510
+ if (options.json === true) {
11511
+ process.stdout.write(`${formatJsonReport(report2)}
11512
+ `);
11513
+ } else {
11514
+ process.stdout.write(`${formatFixCatalog(report2.catalog ?? [])}
11515
+ `);
11516
+ }
11517
+ process.exitCode = report2.success ? 0 : 1;
11518
+ return;
11519
+ }
11520
+ if (options.write === true && options.dryRun === true) {
11521
+ process.stderr.write(
11522
+ "css-doctor: --write and --dry-run are mutually exclusive.\n"
11523
+ );
11524
+ process.exitCode = 1;
11525
+ return;
11526
+ }
11527
+ const dryRun = options.write !== true;
11528
+ const report = await runFix(patterns ?? [], {
11529
+ ...options,
11530
+ dryRun,
11531
+ rules: options.rule ?? options.rules
11532
+ });
11533
+ if (options.json === true) {
11534
+ process.stdout.write(`${formatJsonReport(report)}
11535
+ `);
11536
+ } else if (report.files.length > 0) {
11537
+ process.stdout.write(
11538
+ `${formatFixTerminalReport(
11539
+ report.files.map((file) => file.result),
11540
+ {
11541
+ dryRun: report.dryRun,
11542
+ verbose: options.verbose === true
11543
+ }
11544
+ )}
11545
+ `
11546
+ );
11547
+ if (options.diff === true) {
11548
+ for (const file of report.files) {
11549
+ if (file.diff) {
11550
+ process.stdout.write(`${file.diff}
11551
+ `);
11552
+ }
11553
+ }
11554
+ }
11555
+ if (report.errors.length > 0) {
11556
+ process.stderr.write(
11557
+ `css-doctor: ${report.errors.join("\ncss-doctor: ")}
11558
+ `
11559
+ );
11560
+ }
11561
+ } else {
11562
+ process.stdout.write(
11563
+ `css-doctor fix failed:
11564
+ ${report.errors.map((error) => ` - ${error}`).join("\n")}
11565
+ `
11566
+ );
11567
+ }
11568
+ process.exitCode = report.success ? 0 : 1;
11569
+ }
11570
+ );
11571
+ }
11572
+ async function runFixList() {
9227
11573
  try {
9228
- return (await stat(directoryPath)).isDirectory();
9229
- } catch {
9230
- return false;
11574
+ const binding = loadBinding();
11575
+ const catalog = JSON.parse(binding.listCssFixes());
11576
+ return {
11577
+ schemaVersion: 1,
11578
+ command: "fix",
11579
+ success: true,
11580
+ dryRun: true,
11581
+ errors: [],
11582
+ catalog,
11583
+ files: []
11584
+ };
11585
+ } catch (error) {
11586
+ return {
11587
+ schemaVersion: 1,
11588
+ command: "fix",
11589
+ success: false,
11590
+ dryRun: true,
11591
+ errors: [describeError(error)],
11592
+ files: []
11593
+ };
9231
11594
  }
9232
11595
  }
9233
- async function resolveCssFiles(patterns, cwd) {
9234
- const files = /* @__PURE__ */ new Set();
9235
- const globPatterns = [];
9236
- for (const pattern of patterns) {
9237
- const absolutePath = isAbsolute(pattern) ? pattern : resolve(cwd, pattern);
9238
- if (await isFile(absolutePath)) {
9239
- files.add(absolutePath);
9240
- } else {
9241
- globPatterns.push(pattern);
9242
- }
9243
- }
9244
- if (globPatterns.length > 0) {
9245
- const matches = await (0, import_fast_glob.default)(globPatterns, {
9246
- cwd,
9247
- absolute: true,
9248
- onlyFiles: true,
9249
- unique: true,
9250
- dot: false,
9251
- followSymbolicLinks: true,
9252
- ignore: DEFAULT_IGNORES
9253
- });
9254
- for (const match of matches) {
9255
- files.add(resolve(match));
9256
- }
11596
+ async function runFix(patterns, options = {}) {
11597
+ const cwd = resolve6(options.cwd ?? process.cwd());
11598
+ const dryRun = options.dryRun !== false;
11599
+ const errors = [];
11600
+ const files = [];
11601
+ if (patterns.length === 0) {
11602
+ return {
11603
+ schemaVersion: 1,
11604
+ command: "fix",
11605
+ success: false,
11606
+ dryRun,
11607
+ errors: [
11608
+ 'No CSS files or glob patterns were provided. Example: css-doctor fix "src/**/*.css" --dry-run'
11609
+ ],
11610
+ files: []
11611
+ };
9257
11612
  }
9258
- return [...files].sort((left, right) => left.localeCompare(right));
9259
- }
9260
- async function readCssFile(filePath) {
9261
- try {
9262
- const buffer = await readFile(filePath);
11613
+ if (!await isDirectory(cwd)) {
9263
11614
  return {
9264
- ok: true,
9265
- content: buffer.toString("utf8"),
9266
- sizeBytes: buffer.byteLength
11615
+ schemaVersion: 1,
11616
+ command: "fix",
11617
+ success: false,
11618
+ dryRun,
11619
+ errors: [
11620
+ `Working directory does not exist or is not a directory: ${cwd}`
11621
+ ],
11622
+ files: []
9267
11623
  };
11624
+ }
11625
+ let cssFiles = [];
11626
+ try {
11627
+ cssFiles = await resolveCssFiles(patterns, cwd);
11628
+ if (cssFiles.length === 0) {
11629
+ errors.push(`No CSS files matched: ${patterns.join(", ")}`);
11630
+ }
9268
11631
  } catch (error) {
11632
+ errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
11633
+ }
11634
+ if (errors.length > 0) {
9269
11635
  return {
9270
- ok: false,
9271
- error: `Failed to read "${filePath}": ${describeError(error)}`
11636
+ schemaVersion: 1,
11637
+ command: "fix",
11638
+ success: false,
11639
+ dryRun,
11640
+ errors,
11641
+ files: []
9272
11642
  };
9273
11643
  }
11644
+ try {
11645
+ const binding = loadBinding();
11646
+ for (const absolute of cssFiles) {
11647
+ const displayPath = toDisplayPath(absolute, cwd);
11648
+ const read = await readCssFile(absolute);
11649
+ if (!read.ok) {
11650
+ errors.push(`${displayPath}: ${read.error}`);
11651
+ continue;
11652
+ }
11653
+ let mode;
11654
+ try {
11655
+ mode = (await stat2(absolute)).mode;
11656
+ } catch {
11657
+ mode = void 0;
11658
+ }
11659
+ const result = JSON.parse(
11660
+ binding.applyCssFixes({
11661
+ cssContent: read.content,
11662
+ filePath: displayPath,
11663
+ dryRun,
11664
+ rules: options.rules && options.rules.length > 0 ? options.rules : void 0,
11665
+ includeReview: false
11666
+ })
11667
+ );
11668
+ let diff;
11669
+ if (options.diff === true && result.changed) {
11670
+ diff = formatUnifiedDiff(
11671
+ displayPath,
11672
+ result.originalCss,
11673
+ result.fixedCss
11674
+ );
11675
+ }
11676
+ if (!dryRun && result.changed) {
11677
+ const written = await atomicWriteFile(absolute, result.fixedCss, {
11678
+ mode,
11679
+ backup: options.backup === true
11680
+ });
11681
+ if (!written.ok) {
11682
+ errors.push(written.error);
11683
+ continue;
11684
+ }
11685
+ }
11686
+ files.push({ path: displayPath, result, diff });
11687
+ }
11688
+ } catch (error) {
11689
+ errors.push(describeError(error));
11690
+ }
11691
+ return {
11692
+ schemaVersion: 1,
11693
+ command: "fix",
11694
+ success: errors.length === 0,
11695
+ dryRun,
11696
+ errors,
11697
+ files
11698
+ };
9274
11699
  }
9275
- function toDisplayPath(filePath, cwd) {
9276
- const relativePath = relative(cwd, filePath);
9277
- const displayPath = relativePath.length > 0 ? relativePath : filePath;
9278
- return displayPath.split(sep).join("/");
11700
+ function collectRule(value, previous) {
11701
+ previous.push(value);
11702
+ return previous;
9279
11703
  }
9280
- function describeError(error) {
9281
- if (error instanceof Error) {
9282
- return error.message;
11704
+ function formatFixCatalog(catalog) {
11705
+ const lines = ["", " Available autofix rules", ""];
11706
+ for (const entry of catalog) {
11707
+ lines.push(` [${entry.safety}] ${entry.id}`);
11708
+ lines.push(` ${entry.description}`);
11709
+ lines.push(` ${entry.reason}`);
11710
+ lines.push("");
9283
11711
  }
9284
- return String(error);
11712
+ lines.push(" Only [safe] rules are applied by default.");
11713
+ lines.push("");
11714
+ return lines.join("\n");
9285
11715
  }
9286
11716
 
9287
- // src/commands/check.ts
9288
- var require2 = createRequire(import.meta.url);
9289
- var cachedBinding;
9290
- function loadBinding() {
9291
- if (cachedBinding) {
9292
- return cachedBinding;
11717
+ // src/commands/health.ts
11718
+ init_esm_shims();
11719
+ import { createRequire as createRequire3 } from "module";
11720
+ import { resolve as resolve7 } from "path";
11721
+ var require4 = createRequire3(import.meta.url);
11722
+ var cachedBinding3;
11723
+ function loadBinding3() {
11724
+ if (cachedBinding3) {
11725
+ return cachedBinding3;
9293
11726
  }
9294
11727
  try {
9295
- cachedBinding = require2("@zealakit/css-doctor-binding");
9296
- return cachedBinding;
11728
+ cachedBinding3 = require4("@zealakit/css-doctor-binding");
11729
+ return cachedBinding3;
9297
11730
  } catch (error) {
9298
11731
  throw new Error(
9299
11732
  "Unable to load @zealakit/css-doctor-binding. Build it first with `pnpm --filter @zealakit/css-doctor-binding build`.\n" + describeError(error)
9300
11733
  );
9301
11734
  }
9302
11735
  }
9303
- function registerCheckCommand(program3) {
9304
- program3.command("check").description("Analyze CSS files and report diagnostics").argument(
11736
+ function registerHealthCommand(program3) {
11737
+ program3.command("health").description("Evaluate CSS Health Score with dimension breakdown").argument(
9305
11738
  "[patterns...]",
9306
11739
  "CSS files or glob patterns (quote globs to let css-doctor expand them)"
9307
11740
  ).option("-j, --json", "Output raw JSON for piping and CI/CD").option(
9308
11741
  "--threshold <number>",
9309
- "Warn when a file contains more than <number> selectors",
9310
- parseThreshold
11742
+ "Warn in size/complexity checks when a file has more than <number> selectors",
11743
+ parseNonNegativeInteger3
11744
+ ).option(
11745
+ "--fail-under <number>",
11746
+ "Exit with code 1 when overall health score is below <number> (0-100)",
11747
+ parseScore3
9311
11748
  ).action(async (patterns, options) => {
9312
- const report = await runCheck(patterns ?? [], {
9313
- threshold: options.threshold
11749
+ const report = await runHealth(patterns ?? [], {
11750
+ threshold: options.threshold,
11751
+ failUnder: options.failUnder
9314
11752
  });
9315
11753
  if (options.json === true) {
9316
11754
  process.stdout.write(`${formatJsonReport(report)}
9317
11755
  `);
9318
- } else {
9319
- process.stdout.write(`${formatTerminalReport(report)}
11756
+ } else if (report.health) {
11757
+ process.stdout.write(`${formatHealthTerminalReport(report.health)}
9320
11758
  `);
11759
+ if (report.errors.length > 0) {
11760
+ process.stderr.write(
11761
+ `css-doctor: ${report.errors.join("\ncss-doctor: ")}
11762
+ `
11763
+ );
11764
+ }
11765
+ } else {
11766
+ process.stdout.write(
11767
+ `css-doctor health failed:
11768
+ ${report.errors.map((error) => ` - ${error}`).join("\n")}
11769
+ `
11770
+ );
9321
11771
  }
9322
- process.exitCode = terminalExitCode(report);
11772
+ process.exitCode = healthExitCode(report);
9323
11773
  });
9324
11774
  }
9325
- function parseThreshold(value) {
9326
- const parsed = Number(value);
9327
- if (!Number.isInteger(parsed) || parsed < 0) {
9328
- throw new InvalidArgumentError("threshold must be a non-negative integer");
9329
- }
9330
- return parsed;
9331
- }
9332
- async function runCheck(patterns, options = {}) {
9333
- const startedAt = performance.now();
9334
- const cwd = resolve2(options.cwd ?? process.cwd());
11775
+ async function runHealth(patterns, options = {}) {
11776
+ const cwd = resolve7(options.cwd ?? process.cwd());
9335
11777
  const threshold = options.threshold ?? null;
11778
+ const failUnder = options.failUnder ?? null;
9336
11779
  const errors = [];
9337
- const results = [];
9338
- let files = [];
11780
+ if (failUnder !== null && (failUnder < 0 || failUnder > 100)) {
11781
+ errors.push("--fail-under must be an integer between 0 and 100.");
11782
+ }
9339
11783
  if (threshold !== null && (!Number.isInteger(threshold) || threshold < 0)) {
9340
11784
  errors.push("Threshold must be a non-negative integer.");
9341
11785
  }
11786
+ let files = [];
9342
11787
  if (patterns.length === 0) {
9343
11788
  errors.push(
9344
- 'No CSS files or glob patterns were provided. Example: css-doctor check "src/**/*.css"'
11789
+ 'No CSS files or glob patterns were provided. Example: css-doctor health "src/**/*.css"'
9345
11790
  );
9346
11791
  } else if (!await isDirectory(cwd)) {
9347
11792
  errors.push(
@@ -9357,182 +11802,383 @@ async function runCheck(patterns, options = {}) {
9357
11802
  errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
9358
11803
  }
9359
11804
  }
9360
- if (errors.length === 0) {
9361
- try {
9362
- const analyze = loadBinding().analyzeCss;
9363
- for (const file of files) {
9364
- results.push(await checkFile(file, cwd, threshold, analyze));
9365
- }
9366
- } catch (error) {
9367
- errors.push(describeError(error));
9368
- }
9369
- }
9370
- const issues = results.flatMap((file) => file.issues);
9371
- const warnings = issues.filter((issue) => issue.kind === "syntax").length;
9372
- const analyzed = results.filter((file) => file.ok).length;
9373
- const failed = results.length - analyzed;
9374
- const summary = {
9375
- files: results.length,
9376
- analyzed,
9377
- failed,
9378
- filesWithIssues: results.filter((file) => file.issues.length > 0).length,
9379
- totalSizeBytes: results.reduce((total, file) => total + file.sizeBytes, 0),
9380
- rules: sum(results, (file) => file.result?.stats.ruleCount ?? 0),
9381
- selectors: sum(results, (file) => file.result?.stats.selectorCount ?? 0),
9382
- declarations: sum(
9383
- results,
9384
- (file) => (file.result?.stats.declarationCount ?? 0) + (file.result?.stats.importantDeclarationCount ?? 0)
9385
- ),
9386
- warnings,
9387
- hints: issues.length - warnings,
9388
- issues: issues.length,
9389
- analysisMs: roundMs(
9390
- results.reduce((total, file) => total + file.analysisMs, 0)
9391
- ),
9392
- totalMs: elapsedMs(startedAt)
9393
- };
9394
- return {
9395
- schemaVersion: 1,
9396
- command: "check",
9397
- version: package_default.version,
9398
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9399
- cwd,
9400
- patterns,
9401
- threshold,
9402
- success: errors.length === 0 && failed === 0,
9403
- errors,
9404
- files: results,
9405
- summary
9406
- };
9407
- }
9408
- async function checkFile(filePath, cwd, threshold, analyze) {
9409
- const path2 = toDisplayPath(filePath, cwd);
9410
- const readStartedAt = performance.now();
9411
- const read = await readCssFile(filePath);
9412
- const readMs = elapsedMs(readStartedAt);
9413
- if (!read.ok) {
11805
+ if (errors.length > 0) {
9414
11806
  return {
9415
- path: path2,
9416
- absolutePath: filePath,
9417
- sizeBytes: 0,
9418
- ok: false,
9419
- totalMs: readMs,
9420
- readMs,
9421
- analysisMs: 0,
9422
- issues: [],
9423
- error: read.error
11807
+ schemaVersion: 1,
11808
+ command: "health",
11809
+ success: false,
11810
+ errors,
11811
+ patterns,
11812
+ threshold,
11813
+ failUnder,
11814
+ health: null
9424
11815
  };
9425
11816
  }
9426
- const analysisStartedAt = performance.now();
9427
11817
  try {
9428
- const result = analyze(read.content);
9429
- const analysisMs = elapsedMs(analysisStartedAt);
11818
+ const binding = loadBinding3();
11819
+ const batchFiles = [];
11820
+ for (const file of files) {
11821
+ const read = await readCssFile(file);
11822
+ if (!read.ok) {
11823
+ errors.push(`${toDisplayPath(file, cwd)}: ${read.error}`);
11824
+ continue;
11825
+ }
11826
+ batchFiles.push({
11827
+ cssContent: read.content,
11828
+ filePath: toDisplayPath(file, cwd),
11829
+ sizeBytes: read.sizeBytes
11830
+ });
11831
+ }
11832
+ if (batchFiles.length === 0) {
11833
+ return {
11834
+ schemaVersion: 1,
11835
+ command: "health",
11836
+ success: false,
11837
+ errors: errors.length > 0 ? errors : ["No CSS files could be read for health scoring."],
11838
+ patterns,
11839
+ threshold,
11840
+ failUnder,
11841
+ health: null
11842
+ };
11843
+ }
11844
+ const health = binding.evaluateCssHealthBatch({
11845
+ files: batchFiles,
11846
+ thresholds: threshold === null ? void 0 : {
11847
+ manySelectors: threshold
11848
+ }
11849
+ });
11850
+ const belowFloor = failUnder !== null && health.overall < failUnder ? [
11851
+ `Overall health score ${health.overall} is below --fail-under ${failUnder}.`
11852
+ ] : [];
9430
11853
  return {
9431
- path: path2,
9432
- absolutePath: filePath,
9433
- sizeBytes: read.sizeBytes,
9434
- ok: true,
9435
- totalMs: roundMs(readMs + analysisMs),
9436
- readMs,
9437
- analysisMs,
9438
- issues: collectIssues(result, threshold),
9439
- result
11854
+ schemaVersion: 1,
11855
+ command: "health",
11856
+ success: errors.length === 0 && belowFloor.length === 0,
11857
+ errors: [...errors, ...belowFloor],
11858
+ patterns,
11859
+ threshold,
11860
+ failUnder,
11861
+ health
9440
11862
  };
9441
11863
  } catch (error) {
9442
- const analysisMs = elapsedMs(analysisStartedAt);
9443
11864
  return {
9444
- path: path2,
9445
- absolutePath: filePath,
9446
- sizeBytes: read.sizeBytes,
9447
- ok: false,
9448
- totalMs: roundMs(readMs + analysisMs),
9449
- readMs,
9450
- analysisMs,
9451
- issues: [],
9452
- error: `Failed to analyze file: ${describeError(error)}`
11865
+ schemaVersion: 1,
11866
+ command: "health",
11867
+ success: false,
11868
+ errors: [describeError(error)],
11869
+ patterns,
11870
+ threshold,
11871
+ failUnder,
11872
+ health: null
9453
11873
  };
9454
11874
  }
9455
11875
  }
9456
- function collectIssues(result, threshold) {
9457
- const issues = [];
9458
- for (const warning of result.warnings) {
9459
- issues.push({
9460
- kind: "syntax",
9461
- severity: "warning",
9462
- message: warning.message,
9463
- ...warning.location ? { location: warning.location } : {}
9464
- });
11876
+ function healthExitCode(report) {
11877
+ return report.success ? 0 : 1;
11878
+ }
11879
+ function parseNonNegativeInteger3(value) {
11880
+ const parsed = Number(value);
11881
+ if (!Number.isInteger(parsed) || parsed < 0) {
11882
+ throw new InvalidArgumentError("must be a non-negative integer");
9465
11883
  }
9466
- const parentRuleIndices = new Set(
9467
- result.rules.map((rule) => rule.parentRuleIndex).filter((index) => index !== void 0)
11884
+ return parsed;
11885
+ }
11886
+ function parseScore3(value) {
11887
+ const parsed = Number(value);
11888
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
11889
+ throw new InvalidArgumentError("must be an integer between 0 and 100");
11890
+ }
11891
+ return parsed;
11892
+ }
11893
+
11894
+ // src/commands/optimize.ts
11895
+ init_esm_shims();
11896
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
11897
+ import { dirname as dirname3, join as join2, resolve as resolve8 } from "path";
11898
+ function registerOptimizeCommand(program3) {
11899
+ registerOptimizeLikeCommand(
11900
+ program3,
11901
+ "optimize",
11902
+ "Preview or emit CSS optimization results with explanations"
9468
11903
  );
9469
- for (const rule of result.rules) {
9470
- const hasNoDeclarations = rule.declarationCount === 0 && rule.importantDeclarationCount === 0;
9471
- const hasNoNestedRules = !parentRuleIndices.has(rule.index);
9472
- if (hasNoDeclarations && hasNoNestedRules) {
9473
- const selectorText = rule.selectorIndices.map((index) => result.selectors[index]?.raw).filter((value) => value !== void 0).join(", ");
9474
- issues.push({
9475
- kind: "empty-rule",
9476
- severity: "warning",
9477
- message: selectorText.length > 0 ? `Rule "${selectorText}" has no declarations and no nested rules (dead code candidate).` : "Rule has no declarations and no nested rules (dead code candidate).",
9478
- location: rule.location,
9479
- ...selectorText.length > 0 ? { selector: selectorText } : {}
11904
+ }
11905
+ function registerSplitCommand(program3) {
11906
+ registerOptimizeLikeCommand(
11907
+ program3,
11908
+ "split",
11909
+ "Preview page-coverage / size-budget CSS splitting"
11910
+ );
11911
+ }
11912
+ function registerOptimizeLikeCommand(program3, name, description) {
11913
+ program3.command(name).description(description).argument(
11914
+ "[patterns...]",
11915
+ "CSS files or glob patterns (quote globs to let css-doctor expand them)"
11916
+ ).option("--dry-run", "Analyze and report without writing files").option("--write", "Write emitted chunks to --out-dir").option("--explain", "Include optimization decision explanations").option("--diff", "Include concise unified diffs for emitted CSS").option("-v, --verbose", "Show more decisions and chunks").option("-j, --json", "Emit JSON").option("--report <path>", "Write the JSON report to a file").option("--out-dir <path>", "Directory for emitted chunks when writing").option("--minify", "Minify emitted CSS").option("--no-minify", "Disable minification").option(
11917
+ "--page-usage <json>",
11918
+ "JSON map of page name \u2192 class name arrays for coverage splitting"
11919
+ ).option(
11920
+ "--min-page-ratio <number>",
11921
+ "Minimum coverage ratio for common CSS (0-1)",
11922
+ parseRatio
11923
+ ).option(
11924
+ "--max-chunk-size <bytes>",
11925
+ "Size budget per body chunk in bytes",
11926
+ parsePositiveInteger
11927
+ ).option("--no-page-coverage", "Disable page-coverage splitting").option("--no-size-budget", "Disable size-budget splitting").option("--no-isolate-variables", "Disable CSS variable isolation").action(
11928
+ async (patterns, options) => {
11929
+ if (options.write === true && options.dryRun === true) {
11930
+ process.stderr.write(
11931
+ "css-doctor: --write and --dry-run are mutually exclusive.\n"
11932
+ );
11933
+ process.exitCode = 1;
11934
+ return;
11935
+ }
11936
+ const dryRun = options.write !== true;
11937
+ const report = await runOptimize(patterns ?? [], {
11938
+ ...options,
11939
+ dryRun,
11940
+ commandName: name,
11941
+ minify: options.minify !== false
9480
11942
  });
11943
+ if (options.report) {
11944
+ const reportPath = resolve8(
11945
+ options.cwd ?? process.cwd(),
11946
+ options.report
11947
+ );
11948
+ try {
11949
+ await mkdir3(dirname3(reportPath), { recursive: true });
11950
+ await writeFile3(
11951
+ reportPath,
11952
+ `${formatJsonReport(report)}
11953
+ `,
11954
+ "utf8"
11955
+ );
11956
+ } catch (error) {
11957
+ process.stderr.write(
11958
+ `css-doctor: failed to write --report ${reportPath}: ${describeError(error)}
11959
+ `
11960
+ );
11961
+ process.exitCode = 1;
11962
+ return;
11963
+ }
11964
+ }
11965
+ if (options.json === true) {
11966
+ process.stdout.write(`${formatJsonReport(report)}
11967
+ `);
11968
+ } else if (report.files.length > 0) {
11969
+ for (const file of report.files) {
11970
+ process.stdout.write(
11971
+ `${formatOptimizationTerminalReport(file.report, {
11972
+ dryRun: report.dryRun,
11973
+ verbose: options.verbose === true || options.explain === true
11974
+ })}
11975
+ `
11976
+ );
11977
+ if (options.diff === true && file.diff) {
11978
+ process.stdout.write(`${file.diff}
11979
+ `);
11980
+ }
11981
+ }
11982
+ if (report.errors.length > 0) {
11983
+ process.stderr.write(
11984
+ `css-doctor: ${report.errors.join("\ncss-doctor: ")}
11985
+ `
11986
+ );
11987
+ }
11988
+ } else {
11989
+ process.stdout.write(
11990
+ `css-doctor ${name} failed:
11991
+ ${report.errors.map((error) => ` - ${error}`).join("\n")}
11992
+ `
11993
+ );
11994
+ }
11995
+ process.exitCode = report.success ? 0 : 1;
9481
11996
  }
11997
+ );
11998
+ }
11999
+ async function runOptimize(patterns, options = {}) {
12000
+ const cwd = resolve8(options.cwd ?? process.cwd());
12001
+ const command = options.commandName ?? "optimize";
12002
+ const dryRun = options.dryRun !== false;
12003
+ const errors = [];
12004
+ const warnings = [];
12005
+ const files = [];
12006
+ if (patterns.length === 0) {
12007
+ errors.push(
12008
+ `No CSS files or glob patterns were provided. Example: css-doctor ${command} "src/**/*.css" --dry-run`
12009
+ );
12010
+ } else if (!await isDirectory(cwd)) {
12011
+ errors.push(
12012
+ `Working directory does not exist or is not a directory: ${cwd}`
12013
+ );
9482
12014
  }
9483
- const seenSelectors = /* @__PURE__ */ new Map();
9484
- for (const selector of result.selectors) {
9485
- const first = seenSelectors.get(selector.raw);
9486
- if (!first) {
9487
- seenSelectors.set(selector.raw, selector);
9488
- continue;
12015
+ let pageUsage = {};
12016
+ if (options.pageUsage) {
12017
+ const parsedUsage = parsePageUsage(options.pageUsage);
12018
+ if (!parsedUsage.ok) {
12019
+ errors.push(parsedUsage.error);
12020
+ } else {
12021
+ pageUsage = parsedUsage.value;
9489
12022
  }
9490
- const firstRule = result.rules[first.ruleIndex];
9491
- const currentRule = result.rules[selector.ruleIndex];
9492
- issues.push({
9493
- kind: "duplicate-selector",
9494
- severity: "warning",
9495
- message: `Selector "${selector.raw}" is defined in multiple rules` + (firstRule ? ` (first at line ${firstRule.location.line})` : "") + "; verify whether this is intentional.",
9496
- ...currentRule ? { location: currentRule.location } : {},
9497
- selector: selector.raw
9498
- });
9499
12023
  }
9500
- if (threshold !== null && result.selectors.length > threshold) {
9501
- issues.push({
9502
- kind: "threshold",
9503
- severity: "warning",
9504
- message: `Selector threshold exceeded: ${result.selectors.length} selectors found (threshold: ${threshold}).`
9505
- });
12024
+ let cssFiles = [];
12025
+ if (errors.length === 0) {
12026
+ try {
12027
+ cssFiles = await resolveCssFiles(patterns, cwd);
12028
+ if (cssFiles.length === 0) {
12029
+ errors.push(`No CSS files matched: ${patterns.join(", ")}`);
12030
+ }
12031
+ } catch (error) {
12032
+ errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
12033
+ }
9506
12034
  }
9507
- return issues.sort(compareIssues);
9508
- }
9509
- function compareIssues(left, right) {
9510
- const leftLine = left.location?.line ?? Number.MAX_SAFE_INTEGER;
9511
- const rightLine = right.location?.line ?? Number.MAX_SAFE_INTEGER;
9512
- const lineDifference = leftLine - rightLine;
9513
- if (lineDifference !== 0) {
9514
- return lineDifference;
12035
+ const outDir = options.outDir ? resolve8(cwd, options.outDir) : null;
12036
+ if (!dryRun && !outDir) {
12037
+ errors.push(
12038
+ "Writing chunks requires --out-dir (or omit --write for dry-run)."
12039
+ );
9515
12040
  }
9516
- const columnDifference = (left.location?.column ?? 0) - (right.location?.column ?? 0);
9517
- if (columnDifference !== 0) {
9518
- return columnDifference;
12041
+ if (errors.length > 0) {
12042
+ return {
12043
+ schemaVersion: 1,
12044
+ command,
12045
+ success: false,
12046
+ dryRun,
12047
+ errors,
12048
+ warnings,
12049
+ patterns,
12050
+ files: [],
12051
+ outDir
12052
+ };
9519
12053
  }
9520
- return left.kind.localeCompare(right.kind);
12054
+ try {
12055
+ const binding = loadBinding();
12056
+ const pipelineOptions = {
12057
+ pageUsage,
12058
+ pageCoverage: {
12059
+ enabled: options.noPageCoverage !== true,
12060
+ minPageRatio: options.minPageRatio
12061
+ },
12062
+ sizeBudget: {
12063
+ enabled: options.noSizeBudget !== true,
12064
+ maxChunkSize: options.maxChunkSize,
12065
+ isolateVariables: options.noIsolateVariables !== true
12066
+ },
12067
+ minify: options.minify !== false
12068
+ };
12069
+ for (const absolute of cssFiles) {
12070
+ const read = await readCssFile(absolute);
12071
+ const displayPath = toDisplayPath(absolute, cwd);
12072
+ if (!read.ok) {
12073
+ errors.push(`${displayPath}: ${read.error}`);
12074
+ continue;
12075
+ }
12076
+ const explained = binding.optimizeCssExplained(
12077
+ {
12078
+ cssContent: read.content,
12079
+ ...pipelineOptions
12080
+ },
12081
+ options.verbose === true ? 200 : 40
12082
+ );
12083
+ const report = JSON.parse(explained.reportJson);
12084
+ warnings.push(...report.warnings);
12085
+ const chunks = explained.pipeline.chunks.map((chunk) => ({
12086
+ name: chunk.name,
12087
+ css: chunk.css,
12088
+ sizeBytes: chunk.sizeBytes
12089
+ }));
12090
+ let diff;
12091
+ if (options.diff === true) {
12092
+ const combined = chunks.map((chunk) => `/* ${chunk.name} */
12093
+ ${chunk.css}`).join("\n\n");
12094
+ diff = formatUnifiedDiff(displayPath, read.content, combined, {
12095
+ maxLines: options.verbose === true ? 200 : 80
12096
+ });
12097
+ }
12098
+ if (!dryRun && outDir) {
12099
+ await mkdir3(outDir, { recursive: true });
12100
+ const stem = displayPath.replace(/[\\/]/g, "__").replace(/\.css$/i, "");
12101
+ for (const chunk of chunks) {
12102
+ await writeFile3(
12103
+ join2(outDir, `${stem}.${chunk.name}.css`),
12104
+ chunk.css,
12105
+ "utf8"
12106
+ );
12107
+ }
12108
+ }
12109
+ files.push({ path: displayPath, report, chunks, diff });
12110
+ }
12111
+ } catch (error) {
12112
+ errors.push(describeError(error));
12113
+ }
12114
+ return {
12115
+ schemaVersion: 1,
12116
+ command,
12117
+ success: errors.length === 0,
12118
+ dryRun,
12119
+ errors,
12120
+ warnings,
12121
+ patterns,
12122
+ files,
12123
+ outDir
12124
+ };
9521
12125
  }
9522
- function sum(files, select) {
9523
- return files.reduce((total, file) => total + select(file), 0);
12126
+ function parsePageUsage(raw) {
12127
+ try {
12128
+ const parsed = JSON.parse(raw);
12129
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
12130
+ return {
12131
+ ok: false,
12132
+ error: "--page-usage must be a JSON object of page \u2192 string[]."
12133
+ };
12134
+ }
12135
+ const value = {};
12136
+ for (const [page, classes] of Object.entries(
12137
+ parsed
12138
+ )) {
12139
+ if (!Array.isArray(classes) || classes.some((item) => typeof item !== "string")) {
12140
+ return {
12141
+ ok: false,
12142
+ error: `--page-usage page "${page}" must map to an array of class name strings.`
12143
+ };
12144
+ }
12145
+ value[page] = classes;
12146
+ }
12147
+ return { ok: true, value };
12148
+ } catch (error) {
12149
+ return {
12150
+ ok: false,
12151
+ error: `Invalid --page-usage JSON: ${describeError(error)}`
12152
+ };
12153
+ }
9524
12154
  }
9525
- function elapsedMs(startedAt) {
9526
- return roundMs(performance.now() - startedAt);
12155
+ function parseRatio(value) {
12156
+ const parsed = Number(value);
12157
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
12158
+ throw new InvalidArgumentError("must be a number between 0 and 1");
12159
+ }
12160
+ return parsed;
9527
12161
  }
9528
- function roundMs(value) {
9529
- return Math.round(value * 1e3) / 1e3;
12162
+ function parsePositiveInteger(value) {
12163
+ const parsed = Number(value);
12164
+ if (!Number.isInteger(parsed) || parsed <= 0) {
12165
+ throw new InvalidArgumentError("must be a positive integer");
12166
+ }
12167
+ return parsed;
9530
12168
  }
9531
12169
 
9532
12170
  // src/index.ts
9533
12171
  var program2 = new Command();
9534
- program2.name("css-doctor").description("Diagnose CSS stylesheets with a native lightningcss engine").version(package_default.version, "-v, --version", "Output the version number").showHelpAfterError("(run with --help for usage information)").showSuggestionAfterError().exitOverride();
12172
+ program2.name("css-doctor").description(
12173
+ "Diagnose, score, regress, explain, and safely optimize CSS with a native LightningCSS engine"
12174
+ ).version(package_default.version, "-v, --version", "Output the version number").showHelpAfterError("(run with --help for usage information)").showSuggestionAfterError().exitOverride();
9535
12175
  registerCheckCommand(program2);
12176
+ registerHealthCommand(program2);
12177
+ registerBaselineCommand(program2);
12178
+ registerDiffCommand(program2);
12179
+ registerOptimizeCommand(program2);
12180
+ registerSplitCommand(program2);
12181
+ registerFixCommand(program2);
9536
12182
  try {
9537
12183
  await program2.parseAsync(process.argv);
9538
12184
  } catch (error) {