@saeroon/cli 0.2.8 → 0.2.10

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.
Files changed (2) hide show
  1. package/dist/index.js +248 -33
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2547,9 +2547,8 @@ var VALID_BLOCK_TYPES = /* @__PURE__ */ new Set([
2547
2547
  "main-block",
2548
2548
  "aside-block",
2549
2549
  "article-block",
2550
- // Specialized (5)
2550
+ // Specialized (4)
2551
2551
  "site-menu",
2552
- "floating-action-widget",
2553
2552
  "sticky-cta-bar",
2554
2553
  "cookie-consent-bar",
2555
2554
  "announcement-bar"
@@ -3926,9 +3925,18 @@ import { resolve as resolve13, join as join2 } from "path";
3926
3925
  import { mkdir as mkdir4, writeFile as writeFile8 } from "fs/promises";
3927
3926
 
3928
3927
  // src/scripts/analyze-reference.ts
3929
- import { spawnSync } from "child_process";
3928
+ import { spawnSync, execSync } from "child_process";
3930
3929
  import { writeFile as writeFile7, mkdir as mkdir3 } from "fs/promises";
3931
3930
  import { join } from "path";
3931
+ function getNodeEnvWithGlobalModules() {
3932
+ try {
3933
+ const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
3934
+ const existing = process.env.NODE_PATH || "";
3935
+ return { ...process.env, NODE_PATH: existing ? `${existing}:${globalRoot}` : globalRoot };
3936
+ } catch {
3937
+ return { ...process.env };
3938
+ }
3939
+ }
3932
3940
  var VIEWPORTS = [
3933
3941
  { name: "mobile", width: 375, height: 812 },
3934
3942
  { name: "tablet", width: 768, height: 1024 },
@@ -4026,6 +4034,25 @@ var EXTRACTION_SCRIPT = `
4026
4034
  return 'card-thumbnail';
4027
4035
  }
4028
4036
 
4037
+ // \u2500\u2500 0. \uD31D\uC5C5/\uC624\uBC84\uB808\uC774 DOM \uC81C\uAC70 (\uC139\uC158 \uAC10\uC9C0 \uC804) \u2500\u2500
4038
+ document.querySelectorAll(
4039
+ 'dialog[open], [class*=popup], [class*=layerpopup], [class*=modal]:not(body), [class*=cookie], [class*=consent], [class*=overlay]:not(body), [class*=dimmed]'
4040
+ ).forEach(el => {
4041
+ const rect = el.getBoundingClientRect();
4042
+ const style = window.getComputedStyle(el);
4043
+ const pos = style.position;
4044
+ // fixed/absolute \uC704\uCE58 + \uBDF0\uD3EC\uD2B8 \uB300\uBD80\uBD84 \uCC28\uC9C0 \u2192 \uD31D\uC5C5/\uC624\uBC84\uB808\uC774
4045
+ if ((pos === 'fixed' || pos === 'absolute') && rect.width >= window.innerWidth * 0.5) {
4046
+ el.remove();
4047
+ return;
4048
+ }
4049
+ // leeko-layerpopup \uAC19\uC740 \uD31D\uC5C5: class\uC5D0 popup/modal/overlay \uD3EC\uD568 + \uB2EB\uAE30 \uBC84\uD2BC \uC874\uC7AC
4050
+ if (el.querySelector('[class*=close], [aria-label*=close], button')) {
4051
+ const isSmall = rect.width < 100 && rect.height < 100;
4052
+ if (!isSmall) el.remove();
4053
+ }
4054
+ });
4055
+
4029
4056
  // \u2500\u2500 1. Structure \u2500\u2500
4030
4057
  const sectionSelectors = 'body > header, body > footer, body > nav, body > main, body > section, body > div, body > article, body > aside';
4031
4058
  const topLevelEls = document.querySelectorAll(sectionSelectors);
@@ -4098,6 +4125,39 @@ var EXTRACTION_SCRIPT = `
4098
4125
  return !leafTags.has(tag);
4099
4126
  });
4100
4127
 
4128
+ // \u2500\u2500 Fallback: \uC139\uC158 3\uAC1C \uBBF8\uB9CC\uC774\uBA74 content-based \uD0D0\uC0C9 \u2500\u2500
4129
+ if (sectionEls.length < 3) {
4130
+ const vw = window.innerWidth;
4131
+ const candidates = [];
4132
+ // \uBAA8\uB4E0 \uC694\uC18C\uB97C \uC21C\uD68C\uD558\uBA70 \uC139\uC158 \uD6C4\uBCF4 \uD0D0\uC0C9
4133
+ document.querySelectorAll('header, footer, nav, section, article, aside, div, main').forEach(el => {
4134
+ if (leafTags.has(el.tagName.toLowerCase())) return;
4135
+ const rect = el.getBoundingClientRect();
4136
+ // \uC804\uCCB4 \uD3ED\uC758 80% \uC774\uC0C1 + \uB192\uC774 100px \uC774\uC0C1
4137
+ if (rect.width < vw * 0.8 || rect.height < 100) return;
4138
+ // \uD615\uC81C\uAC00 \uC788\uB294 \uC694\uC18C\uB9CC (\uC139\uC158 = \uAC19\uC740 \uB808\uBCA8 \uD615\uC81C\uB4E4)
4139
+ const parent = el.parentElement;
4140
+ if (!parent) return;
4141
+ const siblings = Array.from(parent.children).filter(s => {
4142
+ const sr = s.getBoundingClientRect();
4143
+ return sr.width >= vw * 0.8 && sr.height >= 100 && !leafTags.has(s.tagName.toLowerCase());
4144
+ });
4145
+ if (siblings.length >= 2) {
4146
+ candidates.push({ el, siblings, parentEl: parent, sibCount: siblings.length });
4147
+ }
4148
+ });
4149
+
4150
+ // \uAC00\uC7A5 \uB9CE\uC740 \uD615\uC81C\uB97C \uAC00\uC9C4 \uADF8\uB8F9 \uC120\uD0DD
4151
+ if (candidates.length > 0) {
4152
+ candidates.sort((a, b) => b.sibCount - a.sibCount);
4153
+ const best = candidates[0];
4154
+ // \uAE30\uC874 \uACB0\uACFC\uBCF4\uB2E4 \uB098\uC740 \uACBD\uC6B0\uB9CC \uAD50\uCCB4
4155
+ if (best.sibCount > sectionEls.length) {
4156
+ sectionEls = best.siblings;
4157
+ }
4158
+ }
4159
+ }
4160
+
4101
4161
  const sections = [];
4102
4162
  let maxDepth = 0;
4103
4163
 
@@ -4256,7 +4316,22 @@ var EXTRACTION_SCRIPT = `
4256
4316
  if (ff) fontFamilySet.add(ff.split(',')[0].trim().replace(/['"]/g, ''));
4257
4317
  });
4258
4318
 
4259
- // 2c. Spacing
4319
+ // 2c. Spacing (B-2: \uC815\uBC00 \uCD94\uCD9C)
4320
+ function median(arr) {
4321
+ if (arr.length === 0) return 0;
4322
+ const s = [...arr].sort((a, b) => a - b);
4323
+ const mid = Math.floor(s.length / 2);
4324
+ return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
4325
+ }
4326
+ function mode(arr) {
4327
+ if (arr.length === 0) return 0;
4328
+ const freq = {};
4329
+ arr.forEach(v => { const k = Math.round(v); freq[k] = (freq[k] || 0) + 1; });
4330
+ return Number(Object.entries(freq).sort((a, b) => b[1] - a[1])[0][0]);
4331
+ }
4332
+ function roundTo4(v) { return Math.round(v / 4) * 4; } // 4px \uB2E8\uC704 \uC815\uADDC\uD654
4333
+
4334
+ // \uC139\uC158 \uAC04 \uAC04\uACA9
4260
4335
  const sectionGaps = [];
4261
4336
  for (let i = 1; i < sections.length; i++) {
4262
4337
  const prev = topLevelEls[i - 1];
@@ -4265,25 +4340,107 @@ var EXTRACTION_SCRIPT = `
4265
4340
  const prevRect = prev.getBoundingClientRect();
4266
4341
  const currRect = curr.getBoundingClientRect();
4267
4342
  const gap = currRect.top - prevRect.bottom;
4268
- if (gap > 0 && gap < 500) sectionGaps.push(gap);
4343
+ if (gap > 0 && gap < 500) sectionGaps.push(Math.round(gap));
4269
4344
  }
4270
4345
  }
4271
4346
 
4272
- const firstContent = document.querySelector('main, [class*=container], [class*=wrapper], body > div > div');
4273
- const contentPadding = firstContent ? parsePixel(getComputedProp(firstContent, 'padding-left')) : 16;
4347
+ // \uC139\uC158 padding-top / padding-bottom \uC218\uC9D1
4348
+ const sectionPaddingTops = [];
4349
+ const sectionPaddingBottoms = [];
4350
+ topLevelEls.forEach(el => {
4351
+ if (!el) return;
4352
+ const pt = parsePixel(getComputedProp(el, 'padding-top'));
4353
+ const pb = parsePixel(getComputedProp(el, 'padding-bottom'));
4354
+ if (pt > 0) sectionPaddingTops.push(Math.round(pt));
4355
+ if (pb > 0) sectionPaddingBottoms.push(Math.round(pb));
4356
+ });
4274
4357
 
4275
- // card gap \uCD94\uC815: \uCCAB \uBC88\uC9F8 grid/flex \uCEE8\uD14C\uC774\uB108\uC758 gap
4276
- let cardGap = 0;
4277
- const gridContainers = document.querySelectorAll('[style*="grid"], [class*="grid"], [style*="flex"]');
4278
- for (const gc of gridContainers) {
4279
- const gap = parsePixel(getComputedProp(gc, 'gap') || getComputedProp(gc, 'column-gap'));
4280
- if (gap > 0) { cardGap = gap; break; }
4281
- }
4358
+ // content padding: \uB2E4\uC218 \uCEE8\uD14C\uC774\uB108\uC5D0\uC11C \uC218\uC9D1
4359
+ const contentPaddingValues = [];
4360
+ const contentPaddingDetails = [];
4361
+ const containerSelectors = 'main, [class*=container], [class*=wrapper], [class*=content], [class*=inner]';
4362
+ document.querySelectorAll(containerSelectors).forEach(el => {
4363
+ const pl = parsePixel(getComputedProp(el, 'padding-left'));
4364
+ const pr = parsePixel(getComputedProp(el, 'padding-right'));
4365
+ if (pl > 0 || pr > 0) {
4366
+ contentPaddingValues.push(pl, pr);
4367
+ contentPaddingDetails.push({ left: Math.round(pl), right: Math.round(pr), count: 1 });
4368
+ }
4369
+ });
4370
+ // \uC911\uBCF5 \uD569\uC0B0
4371
+ const paddingMap = {};
4372
+ contentPaddingDetails.forEach(d => {
4373
+ const k = d.left + ',' + d.right;
4374
+ if (paddingMap[k]) paddingMap[k].count++;
4375
+ else paddingMap[k] = { ...d };
4376
+ });
4377
+ const uniquePaddings = Object.values(paddingMap).sort((a, b) => b.count - a.count);
4378
+ const contentPadding = contentPaddingValues.length > 0 ? mode(contentPaddingValues.filter(v => v > 0)) : 16;
4379
+
4380
+ // card gap: \uBAA8\uB4E0 grid/flex \uCEE8\uD14C\uC774\uB108\uC5D0\uC11C \uC218\uC9D1
4381
+ const cardGapValues = [];
4382
+ const allLayoutContainers = document.querySelectorAll('*');
4383
+ allLayoutContainers.forEach(el => {
4384
+ const display = getComputedProp(el, 'display');
4385
+ if (!display || (!display.includes('grid') && !display.includes('flex'))) return;
4386
+ const gap = parsePixel(getComputedProp(el, 'gap'));
4387
+ const colGap = parsePixel(getComputedProp(el, 'column-gap'));
4388
+ const rowGap = parsePixel(getComputedProp(el, 'row-gap'));
4389
+ const v = gap || colGap || rowGap;
4390
+ if (v > 0 && v < 200) cardGapValues.push(Math.round(v));
4391
+ });
4392
+ // margin \uAE30\uBC18 \uAC04\uACA9\uB3C4 \uAC10\uC9C0 (flex/grid \uC544\uB2CC \uB9AC\uC2A4\uD2B8)
4393
+ document.querySelectorAll('ul, ol, [class*=list], [class*=cards]').forEach(parent => {
4394
+ const children = Array.from(parent.children);
4395
+ for (let i = 1; i < Math.min(children.length, 5); i++) {
4396
+ const prevRect = children[i - 1].getBoundingClientRect();
4397
+ const currRect = children[i].getBoundingClientRect();
4398
+ const vGap = currRect.top - prevRect.bottom;
4399
+ const hGap = currRect.left - prevRect.right;
4400
+ const g = Math.abs(vGap) < 2 ? hGap : vGap; // \uC218\uD3C9 \uBC30\uCE58\uBA74 hGap
4401
+ if (g > 0 && g < 200) cardGapValues.push(Math.round(g));
4402
+ }
4403
+ });
4404
+ const cardGap = cardGapValues.length > 0 ? mode(cardGapValues) : 16;
4405
+
4406
+ // \uC139\uC158 \uB0B4 \uD615\uC81C \uC694\uC18C \uAC04 \uC218\uC9C1 \uAC04\uACA9 (elementGap)
4407
+ const elementGapValues = [];
4408
+ topLevelEls.forEach(sectionEl => {
4409
+ if (!sectionEl) return;
4410
+ const children = Array.from(sectionEl.children);
4411
+ for (let i = 1; i < Math.min(children.length, 8); i++) {
4412
+ const prevRect = children[i - 1].getBoundingClientRect();
4413
+ const currRect = children[i].getBoundingClientRect();
4414
+ const gap = currRect.top - prevRect.bottom;
4415
+ if (gap > 0 && gap < 200) elementGapValues.push(Math.round(gap));
4416
+ }
4417
+ });
4418
+ const elementGap = elementGapValues.length > 0 ? median(elementGapValues) : 16;
4282
4419
 
4283
- // base unit \uCD94\uC815 (\uAC00\uC7A5 \uD754\uD55C \uAC04\uACA9\uAC12\uC758 \uCD5C\uB300\uACF5\uC57D\uC218)
4284
- const allGaps = [...sectionGaps, contentPadding, cardGap].filter(v => v > 0);
4420
+ // \uBE48\uB3C4 \uBD84\uD3EC (4px \uB2E8\uC704\uB85C \uC815\uADDC\uD654)
4421
+ const spacingFrequency = {};
4422
+ [...sectionGaps, ...sectionPaddingTops, ...sectionPaddingBottoms, ...contentPaddingValues, ...cardGapValues, ...elementGapValues]
4423
+ .filter(v => v > 0)
4424
+ .forEach(v => { const k = roundTo4(v); spacingFrequency[k] = (spacingFrequency[k] || 0) + 1; });
4425
+
4426
+ // base unit: \uBE48\uB3C4 \uCD5C\uB2E4 \uAC12 \uAE30\uBC18, 4/8 \uCCB4\uACC4 \uC790\uB3D9 \uAC10\uC9C0
4427
+ const freqEntries = Object.entries(spacingFrequency).sort((a, b) => b[1] - a[1]);
4428
+ const topValues = freqEntries.slice(0, 6).map(e => Number(e[0]));
4285
4429
  function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
4286
- const baseUnit = allGaps.length > 1 ? allGaps.reduce((a, b) => gcd(a, b)) : (allGaps[0] || 8);
4430
+ const baseUnit = topValues.length > 1 ? topValues.reduce((a, b) => gcd(a, b)) : (topValues[0] || 8);
4431
+
4432
+ // spacing details \uAC1D\uCCB4
4433
+ const spacingDetails = {
4434
+ sectionGaps,
4435
+ sectionPadding: {
4436
+ top: sectionPaddingTops.length > 0 ? median(sectionPaddingTops) : 0,
4437
+ bottom: sectionPaddingBottoms.length > 0 ? median(sectionPaddingBottoms) : 0,
4438
+ },
4439
+ contentPaddings: uniquePaddings.slice(0, 5),
4440
+ cardGaps: [...new Set(cardGapValues)].sort((a, b) => a - b).slice(0, 10),
4441
+ elementGap,
4442
+ spacingFrequency,
4443
+ };
4287
4444
 
4288
4445
  // 2d. BorderRadius
4289
4446
  const radiusValues = [];
@@ -4596,10 +4753,11 @@ var EXTRACTION_SCRIPT = `
4596
4753
  scale: typographyScale,
4597
4754
  },
4598
4755
  spacing: {
4599
- sectionGap: sectionGaps.length > 0 ? Math.round(sectionGaps.reduce((a, b) => a + b, 0) / sectionGaps.length) : 80,
4756
+ sectionGap: sectionGaps.length > 0 ? median(sectionGaps) : 80,
4600
4757
  contentPadding,
4601
4758
  cardGap: cardGap || 16,
4602
4759
  baseUnit: Math.max(baseUnit, 4),
4760
+ details: spacingDetails,
4603
4761
  },
4604
4762
  borderRadius: {
4605
4763
  small: uniqueRadii[0] || 0,
@@ -4720,7 +4878,7 @@ function captureScreenshot(url, outputPath, width, height, timeout) {
4720
4878
  const result = spawnSync("node", ["-e", scriptContent], {
4721
4879
  stdio: "pipe",
4722
4880
  timeout: timeout + 3e4,
4723
- env: { ...process.env }
4881
+ env: getNodeEnvWithGlobalModules()
4724
4882
  });
4725
4883
  if (result.status !== 0) {
4726
4884
  const stderr = result.stderr?.toString() ?? "";
@@ -4789,7 +4947,7 @@ async function extractPageData(url, timeout) {
4789
4947
  const result = spawnSync("node", ["-e", scriptContent], {
4790
4948
  stdio: "pipe",
4791
4949
  timeout: timeout + 3e4,
4792
- env: { ...process.env }
4950
+ env: getNodeEnvWithGlobalModules()
4793
4951
  });
4794
4952
  if (result.status !== 0) {
4795
4953
  const stderr = result.stderr?.toString() ?? "";
@@ -5033,7 +5191,17 @@ async function commandAnalyze(url, options) {
5033
5191
  console.log(chalk15.dim(` Accent: ${colors.accent}`));
5034
5192
  console.log(chalk15.dim(` \uD3F0\uD2B8: ${typography.fontFamilies.join(", ") || "(\uCD94\uCD9C \uC2E4\uD328)"}`));
5035
5193
  console.log(chalk15.dim(` \uC139\uC158 \uAC04\uACA9: ${spacing.sectionGap}px`));
5194
+ console.log(chalk15.dim(` \uCF58\uD150\uCE20 \uD328\uB529: ${spacing.contentPadding}px`));
5195
+ console.log(chalk15.dim(` \uCE74\uB4DC \uAC04\uACA9: ${spacing.cardGap}px`));
5036
5196
  console.log(chalk15.dim(` \uAE30\uBCF8 \uB2E8\uC704: ${spacing.baseUnit}px`));
5197
+ if (spacing.details) {
5198
+ const d = spacing.details;
5199
+ console.log(chalk15.dim(` \uC139\uC158 \uD328\uB529: top ${d.sectionPadding.top}px / bottom ${d.sectionPadding.bottom}px`));
5200
+ console.log(chalk15.dim(` \uC694\uC18C \uAC04\uACA9: ${d.elementGap}px`));
5201
+ if (d.cardGaps.length > 1) {
5202
+ console.log(chalk15.dim(` \uCE74\uB4DC \uAC04\uACA9 \uBD84\uD3EC: [${d.cardGaps.join(", ")}]px`));
5203
+ }
5204
+ }
5037
5205
  console.log("");
5038
5206
  console.log(chalk15.bold("\uC778\uD130\uB799\uC158"));
5039
5207
  const { interactions } = analysis;
@@ -5177,6 +5345,15 @@ import chalk16 from "chalk";
5177
5345
  import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
5178
5346
  import { resolve as resolve14, join as join3 } from "path";
5179
5347
  import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
5348
+ function getNodeEnvWithGlobalModules2() {
5349
+ try {
5350
+ const globalRoot = execSync2("npm root -g", { encoding: "utf-8" }).trim();
5351
+ const existing = process.env.NODE_PATH || "";
5352
+ return { ...process.env, NODE_PATH: existing ? `${existing}:${globalRoot}` : globalRoot };
5353
+ } catch {
5354
+ return { ...process.env };
5355
+ }
5356
+ }
5180
5357
  async function commandCompare(options) {
5181
5358
  console.log(chalk16.bold("\n\uC2DC\uAC01 \uBE44\uAD50 (Visual Diff)\n"));
5182
5359
  if (!options.ref || !options.preview) {
@@ -5214,6 +5391,27 @@ async function runMultiViewportCompare(options) {
5214
5391
  console.log("");
5215
5392
  const hasMagick = checkCommand("magick");
5216
5393
  const results = [];
5394
+ const baselineMap = /* @__PURE__ */ new Map();
5395
+ if (hasMagick) {
5396
+ const blSpinner = spinner("\uAE30\uC900\uC120 \uCE21\uC815 \uC911 (\uB808\uD37C\uB7F0\uC2A4 2\uD68C \uCEA1\uCC98)...");
5397
+ for (const vp of viewports) {
5398
+ const blRef1 = join3(outputDir, `baseline-1-${vp.name}.png`);
5399
+ const blRef2 = join3(outputDir, `baseline-2-${vp.name}.png`);
5400
+ const blDiff = join3(outputDir, `baseline-diff-${vp.name}.png`);
5401
+ try {
5402
+ captureScreenshot2(options.ref, blRef1, vp.width, vp.height);
5403
+ captureScreenshot2(options.ref, blRef2, vp.width, vp.height);
5404
+ const blPct = generateDiffWithMagick(blRef1, blRef2, blDiff);
5405
+ baselineMap.set(vp.name, blPct >= 0 ? blPct : 0);
5406
+ } catch {
5407
+ baselineMap.set(vp.name, 0);
5408
+ }
5409
+ }
5410
+ const blValues = [...baselineMap.values()];
5411
+ const blAvg = blValues.reduce((a, b) => a + b, 0) / blValues.length;
5412
+ blSpinner.stop(chalk16.dim(` \uAE30\uC900\uC120 \uB178\uC774\uC988: \uD3C9\uADE0 ${blAvg.toFixed(1)}% (${blValues.map((v, i) => `${viewports[i].name} ${v.toFixed(1)}%`).join(", ")})`));
5413
+ console.log("");
5414
+ }
5217
5415
  for (const vp of viewports) {
5218
5416
  const vpSpinner = spinner(`${vp.name} (${vp.width}px) \uBE44\uAD50 \uC911...`);
5219
5417
  const refPath = join3(outputDir, `ref-${vp.name}.png`);
@@ -5229,17 +5427,23 @@ async function runMultiViewportCompare(options) {
5229
5427
  generateSideBySide(refPath, previewPath, diffPath);
5230
5428
  diffPercentage = -1;
5231
5429
  }
5430
+ const baselinePct = baselineMap.get(vp.name) ?? 0;
5431
+ const adjustedPct = diffPercentage >= 0 ? Math.round(Math.max(0, diffPercentage - baselinePct) * 10) / 10 : -1;
5232
5432
  results.push({
5233
5433
  name: vp.name,
5234
5434
  width: vp.width,
5235
5435
  referenceScreenshot: refPath,
5236
5436
  previewScreenshot: previewPath,
5237
5437
  diffScreenshot: diffPath,
5238
- diffPercentage
5438
+ diffPercentage,
5439
+ baselineDiffPercentage: baselinePct,
5440
+ adjustedDiffPercentage: adjustedPct
5239
5441
  });
5240
- const pctText = diffPercentage >= 0 ? `${diffPercentage.toFixed(1)}%` : "(\uACC4\uC0B0 \uBD88\uAC00)";
5241
- const pctColor = diffPercentage <= 10 ? chalk16.green : diffPercentage <= 25 ? chalk16.yellow : chalk16.red;
5242
- vpSpinner.stop(` ${vp.name}: diff ${pctColor(pctText)}`);
5442
+ const rawText = diffPercentage >= 0 ? `${diffPercentage.toFixed(1)}%` : "(\uACC4\uC0B0 \uBD88\uAC00)";
5443
+ const adjText = adjustedPct >= 0 ? `${adjustedPct.toFixed(1)}%` : "";
5444
+ const adjColor = adjustedPct <= 10 ? chalk16.green : adjustedPct <= 25 ? chalk16.yellow : chalk16.red;
5445
+ const label = baselinePct > 0 ? ` ${vp.name}: raw ${rawText} \u2192 ${chalk16.bold("adjusted")} ${adjColor(adjText)} ${chalk16.dim(`(baseline ${baselinePct.toFixed(1)}%)`)}` : ` ${vp.name}: diff ${adjColor(rawText)}`;
5446
+ vpSpinner.stop(label);
5243
5447
  } catch (error) {
5244
5448
  vpSpinner.stop(chalk16.red(` ${vp.name}: \uC2E4\uD328 \u2014 ${error instanceof Error ? error.message : String(error)}`));
5245
5449
  results.push({
@@ -5254,27 +5458,38 @@ async function runMultiViewportCompare(options) {
5254
5458
  }
5255
5459
  const validResults = results.filter((r) => r.diffPercentage >= 0);
5256
5460
  const overallDiff = validResults.length > 0 ? validResults.reduce((sum, r) => sum + r.diffPercentage, 0) / validResults.length : -1;
5461
+ const adjustedResults = results.filter((r) => (r.adjustedDiffPercentage ?? -1) >= 0);
5462
+ const overallAdjusted = adjustedResults.length > 0 ? adjustedResults.reduce((sum, r) => sum + r.adjustedDiffPercentage, 0) / adjustedResults.length : void 0;
5257
5463
  const report = {
5258
5464
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5259
5465
  referenceUrl: options.ref,
5260
5466
  previewUrl: options.preview,
5261
5467
  viewports: results,
5262
- overallDiffPercentage: Math.round(overallDiff * 10) / 10
5468
+ overallDiffPercentage: Math.round(overallDiff * 10) / 10,
5469
+ overallAdjustedDiffPercentage: overallAdjusted != null ? Math.round(overallAdjusted * 10) / 10 : void 0
5263
5470
  };
5264
5471
  const reportPath = join3(outputDir, "comparison-report.json");
5265
5472
  await writeFile9(reportPath, JSON.stringify(report, null, 2), "utf-8");
5266
5473
  console.log("");
5267
5474
  console.log(chalk16.bold("\uBE44\uAD50 \uC694\uC57D"));
5475
+ const hasBaseline = results.some((r) => (r.baselineDiffPercentage ?? 0) > 0);
5268
5476
  for (const r of results) {
5269
- const pct = r.diffPercentage >= 0 ? `${r.diffPercentage.toFixed(1)}%` : "N/A";
5270
- const icon = r.diffPercentage <= 10 ? chalk16.green("\u25CF") : r.diffPercentage <= 25 ? chalk16.yellow("\u25CF") : chalk16.red("\u25CF");
5271
- console.log(` ${icon} ${r.name.padEnd(8)} ${pct.padStart(6)} ${chalk16.dim(r.diffScreenshot)}`);
5477
+ const adjPct = r.adjustedDiffPercentage ?? r.diffPercentage;
5478
+ const displayPct = adjPct >= 0 ? `${adjPct.toFixed(1)}%` : "N/A";
5479
+ const icon = adjPct <= 10 ? chalk16.green("\u25CF") : adjPct <= 25 ? chalk16.yellow("\u25CF") : chalk16.red("\u25CF");
5480
+ const blNote = hasBaseline && (r.baselineDiffPercentage ?? 0) > 0 ? chalk16.dim(` (raw ${r.diffPercentage.toFixed(1)}%, baseline \u2212${r.baselineDiffPercentage.toFixed(1)}%)`) : "";
5481
+ console.log(` ${icon} ${r.name.padEnd(8)} ${displayPct.padStart(6)}${blNote}`);
5272
5482
  }
5483
+ const judgeDiff = overallAdjusted ?? overallDiff;
5273
5484
  console.log("");
5274
- if (overallDiff >= 0) {
5275
- const overallColor = overallDiff <= 10 ? chalk16.green : overallDiff <= 25 ? chalk16.yellow : chalk16.red;
5276
- console.log(chalk16.bold(`\uC804\uCCB4 \uD3C9\uADE0 diff: ${overallColor(`${overallDiff.toFixed(1)}%`)}`));
5277
- if (overallDiff <= 10) {
5485
+ if (judgeDiff >= 0) {
5486
+ const overallColor = judgeDiff <= 10 ? chalk16.green : judgeDiff <= 25 ? chalk16.yellow : chalk16.red;
5487
+ const label = overallAdjusted != null ? "\uC804\uCCB4 \uD3C9\uADE0 diff (adjusted)" : "\uC804\uCCB4 \uD3C9\uADE0 diff";
5488
+ console.log(chalk16.bold(`${label}: ${overallColor(`${judgeDiff.toFixed(1)}%`)}`));
5489
+ if (overallAdjusted != null && overallAdjusted !== overallDiff) {
5490
+ console.log(chalk16.dim(` (raw \uD3C9\uADE0: ${overallDiff.toFixed(1)}%, \uAE30\uC900\uC120 \uCC28\uAC10: \u2212${(overallDiff - overallAdjusted).toFixed(1)}%)`));
5491
+ }
5492
+ if (judgeDiff <= 10) {
5278
5493
  console.log(chalk16.green(" \u2192 \uBE44\uAD50 \uB8E8\uD504 \uC644\uB8CC! CEO \uCD5C\uC885 \uD655\uC778 \uC694\uCCAD \uAC00\uB2A5"));
5279
5494
  } else {
5280
5495
  console.log(chalk16.yellow(" \u2192 diff > 10%: Vision \uBE44\uAD50 \u2192 \uC2A4\uD0A4\uB9C8 \uC218\uC815 \uD544\uC694"));
@@ -5461,7 +5676,7 @@ function captureScreenshot2(url, outputPath, width, height) {
5461
5676
  const result = spawnSync2("node", ["-e", scriptContent], {
5462
5677
  stdio: "pipe",
5463
5678
  timeout: 9e4,
5464
- env: { ...process.env }
5679
+ env: getNodeEnvWithGlobalModules2()
5465
5680
  });
5466
5681
  if (result.status !== 0) {
5467
5682
  const stderr = result.stderr?.toString() ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saeroon/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Saeroon Hosting developer CLI — preview, validate, and deploy sites & templates",
5
5
  "private": false,
6
6
  "type": "module",