@agent-scope/cli 1.12.0 → 1.14.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.cjs CHANGED
@@ -228,9 +228,9 @@ function createRL() {
228
228
  });
229
229
  }
230
230
  async function ask(rl, question) {
231
- return new Promise((resolve11) => {
231
+ return new Promise((resolve14) => {
232
232
  rl.question(question, (answer) => {
233
- resolve11(answer.trim());
233
+ resolve14(answer.trim());
234
234
  });
235
235
  });
236
236
  }
@@ -379,7 +379,7 @@ function createInitCommand() {
379
379
  }
380
380
  async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
381
381
  const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
382
- return wrapInHtml(bundledScript, viewportWidth);
382
+ return wrapInHtml(bundledScript, viewportWidth, projectCss);
383
383
  }
384
384
  async function bundleComponentToIIFE(filePath, componentName, props) {
385
385
  const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
@@ -466,7 +466,9 @@ ${msg}`);
466
466
  return outputFile.text;
467
467
  }
468
468
  function wrapInHtml(bundledScript, viewportWidth, projectCss) {
469
- const projectStyleBlock = "";
469
+ const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
470
+ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
471
+ </style>` : "";
470
472
  return `<!DOCTYPE html>
471
473
  <html lang="en">
472
474
  <head>
@@ -2830,8 +2832,8 @@ Available: ${available}`
2830
2832
  `
2831
2833
  );
2832
2834
  if (opts.sprite !== void 0) {
2833
- const { SpriteSheetGenerator } = await import('@agent-scope/render');
2834
- const gen = new SpriteSheetGenerator();
2835
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
2836
+ const gen = new SpriteSheetGenerator2();
2835
2837
  const sheet = await gen.generate(result);
2836
2838
  const spritePath = path.resolve(process.cwd(), opts.sprite);
2837
2839
  fs.writeFileSync(spritePath, sheet.png);
@@ -2840,8 +2842,8 @@ Available: ${available}`
2840
2842
  }
2841
2843
  const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
2842
2844
  if (fmt === "file") {
2843
- const { SpriteSheetGenerator } = await import('@agent-scope/render');
2844
- const gen = new SpriteSheetGenerator();
2845
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
2846
+ const gen = new SpriteSheetGenerator2();
2845
2847
  const sheet = await gen.generate(result);
2846
2848
  const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
2847
2849
  fs.mkdirSync(dir, { recursive: true });
@@ -2858,8 +2860,8 @@ Available: ${available}`
2858
2860
  } else if (fmt === "png") {
2859
2861
  if (opts.sprite !== void 0) {
2860
2862
  } else {
2861
- const { SpriteSheetGenerator } = await import('@agent-scope/render');
2862
- const gen = new SpriteSheetGenerator();
2863
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
2864
+ const gen = new SpriteSheetGenerator2();
2863
2865
  const sheet = await gen.generate(result);
2864
2866
  process.stdout.write(sheet.png);
2865
2867
  }
@@ -3166,12 +3168,12 @@ async function runBaseline(options = {}) {
3166
3168
  fs.mkdirSync(rendersDir, { recursive: true });
3167
3169
  let manifest$1;
3168
3170
  if (manifestPath !== void 0) {
3169
- const { readFileSync: readFileSync8 } = await import('fs');
3171
+ const { readFileSync: readFileSync10 } = await import('fs');
3170
3172
  const absPath = path.resolve(rootDir, manifestPath);
3171
3173
  if (!fs.existsSync(absPath)) {
3172
3174
  throw new Error(`Manifest not found at ${absPath}.`);
3173
3175
  }
3174
- manifest$1 = JSON.parse(readFileSync8(absPath, "utf-8"));
3176
+ manifest$1 = JSON.parse(readFileSync10(absPath, "utf-8"));
3175
3177
  process.stderr.write(`Loaded manifest from ${manifestPath}
3176
3178
  `);
3177
3179
  } else {
@@ -3332,6 +3334,479 @@ function registerBaselineSubCommand(reportCmd) {
3332
3334
  }
3333
3335
  );
3334
3336
  }
3337
+ var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
3338
+ function loadBaselineCompliance(baselineDir) {
3339
+ const compliancePath = path.resolve(baselineDir, "compliance.json");
3340
+ if (!fs.existsSync(compliancePath)) return null;
3341
+ const raw = JSON.parse(fs.readFileSync(compliancePath, "utf-8"));
3342
+ return raw;
3343
+ }
3344
+ function loadBaselineRenderJson(baselineDir, componentName) {
3345
+ const jsonPath = path.resolve(baselineDir, "renders", `${componentName}.json`);
3346
+ if (!fs.existsSync(jsonPath)) return null;
3347
+ return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
3348
+ }
3349
+ var _pool5 = null;
3350
+ async function getPool5(viewportWidth, viewportHeight) {
3351
+ if (_pool5 === null) {
3352
+ _pool5 = new render.BrowserPool({
3353
+ size: { browsers: 1, pagesPerBrowser: 4 },
3354
+ viewportWidth,
3355
+ viewportHeight
3356
+ });
3357
+ await _pool5.init();
3358
+ }
3359
+ return _pool5;
3360
+ }
3361
+ async function shutdownPool5() {
3362
+ if (_pool5 !== null) {
3363
+ await _pool5.close();
3364
+ _pool5 = null;
3365
+ }
3366
+ }
3367
+ async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
3368
+ const pool = await getPool5(viewportWidth, viewportHeight);
3369
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
3370
+ const slot = await pool.acquire();
3371
+ const { page } = slot;
3372
+ try {
3373
+ await page.setContent(htmlHarness, { waitUntil: "load" });
3374
+ await page.waitForFunction(
3375
+ () => {
3376
+ const w = window;
3377
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
3378
+ },
3379
+ { timeout: 15e3 }
3380
+ );
3381
+ const renderError = await page.evaluate(() => {
3382
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
3383
+ });
3384
+ if (renderError !== null) {
3385
+ throw new Error(`Component render error: ${renderError}`);
3386
+ }
3387
+ const rootDir = process.cwd();
3388
+ const classes = await page.evaluate(() => {
3389
+ const set = /* @__PURE__ */ new Set();
3390
+ document.querySelectorAll("[class]").forEach((el) => {
3391
+ for (const c of el.className.split(/\s+/)) {
3392
+ if (c) set.add(c);
3393
+ }
3394
+ });
3395
+ return [...set];
3396
+ });
3397
+ const projectCss = await getCompiledCssForClasses(rootDir, classes);
3398
+ if (projectCss != null && projectCss.length > 0) {
3399
+ await page.addStyleTag({ content: projectCss });
3400
+ }
3401
+ const startMs = performance.now();
3402
+ const rootLocator = page.locator("[data-reactscope-root]");
3403
+ const boundingBox = await rootLocator.boundingBox();
3404
+ if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
3405
+ throw new Error(
3406
+ `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3407
+ );
3408
+ }
3409
+ const PAD = 24;
3410
+ const MIN_W = 320;
3411
+ const MIN_H = 200;
3412
+ const clipX = Math.max(0, boundingBox.x - PAD);
3413
+ const clipY = Math.max(0, boundingBox.y - PAD);
3414
+ const rawW = boundingBox.width + PAD * 2;
3415
+ const rawH = boundingBox.height + PAD * 2;
3416
+ const clipW = Math.max(rawW, MIN_W);
3417
+ const clipH = Math.max(rawH, MIN_H);
3418
+ const safeW = Math.min(clipW, viewportWidth - clipX);
3419
+ const safeH = Math.min(clipH, viewportHeight - clipY);
3420
+ const screenshot = await page.screenshot({
3421
+ clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3422
+ type: "png"
3423
+ });
3424
+ const computedStylesRaw = {};
3425
+ const styles = await page.evaluate((sel) => {
3426
+ const el = document.querySelector(sel);
3427
+ if (el === null) return {};
3428
+ const computed = window.getComputedStyle(el);
3429
+ const out = {};
3430
+ for (const prop of [
3431
+ "display",
3432
+ "width",
3433
+ "height",
3434
+ "color",
3435
+ "backgroundColor",
3436
+ "fontSize",
3437
+ "fontFamily",
3438
+ "padding",
3439
+ "margin"
3440
+ ]) {
3441
+ out[prop] = computed.getPropertyValue(prop);
3442
+ }
3443
+ return out;
3444
+ }, "[data-reactscope-root] > *");
3445
+ computedStylesRaw["[data-reactscope-root] > *"] = styles;
3446
+ const renderTimeMs = performance.now() - startMs;
3447
+ return {
3448
+ screenshot,
3449
+ width: Math.round(safeW),
3450
+ height: Math.round(safeH),
3451
+ renderTimeMs,
3452
+ computedStyles: computedStylesRaw
3453
+ };
3454
+ } finally {
3455
+ pool.release(slot);
3456
+ }
3457
+ }
3458
+ function extractComputedStyles2(computedStylesRaw) {
3459
+ const flat = {};
3460
+ for (const styles of Object.values(computedStylesRaw)) {
3461
+ Object.assign(flat, styles);
3462
+ }
3463
+ const colors = {};
3464
+ const spacing = {};
3465
+ const typography = {};
3466
+ const borders = {};
3467
+ const shadows = {};
3468
+ for (const [prop, value] of Object.entries(flat)) {
3469
+ if (prop === "color" || prop === "backgroundColor") {
3470
+ colors[prop] = value;
3471
+ } else if (prop === "padding" || prop === "margin") {
3472
+ spacing[prop] = value;
3473
+ } else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
3474
+ typography[prop] = value;
3475
+ } else if (prop === "borderRadius" || prop === "borderWidth") {
3476
+ borders[prop] = value;
3477
+ } else if (prop === "boxShadow") {
3478
+ shadows[prop] = value;
3479
+ }
3480
+ }
3481
+ return { colors, spacing, typography, borders, shadows };
3482
+ }
3483
+ function classifyComponent(entry, regressionThreshold) {
3484
+ if (entry.renderFailed) return "unchanged";
3485
+ if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
3486
+ return "added";
3487
+ }
3488
+ if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
3489
+ return "removed";
3490
+ }
3491
+ const delta = entry.complianceDelta;
3492
+ if (delta !== null) {
3493
+ if (delta <= -regressionThreshold) return "compliance_regressed";
3494
+ if (delta >= regressionThreshold) return "compliance_improved";
3495
+ }
3496
+ if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
3497
+ const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
3498
+ const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
3499
+ if (dw > 10 || dh > 10) return "size_changed";
3500
+ }
3501
+ return "unchanged";
3502
+ }
3503
+ async function runDiff(options = {}) {
3504
+ const {
3505
+ baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
3506
+ componentsGlob,
3507
+ manifestPath,
3508
+ viewportWidth = 375,
3509
+ viewportHeight = 812,
3510
+ regressionThreshold = 0.01
3511
+ } = options;
3512
+ const startTime = performance.now();
3513
+ const rootDir = process.cwd();
3514
+ const baselineDir = path.resolve(rootDir, baselineDirRaw);
3515
+ if (!fs.existsSync(baselineDir)) {
3516
+ throw new Error(
3517
+ `Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
3518
+ );
3519
+ }
3520
+ const baselineManifestPath = path.resolve(baselineDir, "manifest.json");
3521
+ if (!fs.existsSync(baselineManifestPath)) {
3522
+ throw new Error(
3523
+ `Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
3524
+ );
3525
+ }
3526
+ const baselineManifest = JSON.parse(fs.readFileSync(baselineManifestPath, "utf-8"));
3527
+ const baselineCompliance = loadBaselineCompliance(baselineDir);
3528
+ const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
3529
+ process.stderr.write(
3530
+ `Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
3531
+ `
3532
+ );
3533
+ let currentManifest;
3534
+ if (manifestPath !== void 0) {
3535
+ const absPath = path.resolve(rootDir, manifestPath);
3536
+ if (!fs.existsSync(absPath)) {
3537
+ throw new Error(`Manifest not found at "${absPath}".`);
3538
+ }
3539
+ currentManifest = JSON.parse(fs.readFileSync(absPath, "utf-8"));
3540
+ process.stderr.write(`Loaded manifest from ${manifestPath}
3541
+ `);
3542
+ } else {
3543
+ process.stderr.write("Scanning for React components\u2026\n");
3544
+ currentManifest = await manifest.generateManifest({ rootDir });
3545
+ const count = Object.keys(currentManifest.components).length;
3546
+ process.stderr.write(`Found ${count} components.
3547
+ `);
3548
+ }
3549
+ let componentNames = Object.keys(currentManifest.components);
3550
+ if (componentsGlob !== void 0) {
3551
+ componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
3552
+ process.stderr.write(
3553
+ `Filtered to ${componentNames.length} components matching "${componentsGlob}".
3554
+ `
3555
+ );
3556
+ }
3557
+ const removedNames = [...baselineComponentNames].filter(
3558
+ (name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
3559
+ );
3560
+ const total = componentNames.length;
3561
+ process.stderr.write(`Rendering ${total} components for diff\u2026
3562
+ `);
3563
+ const computedStylesMap = /* @__PURE__ */ new Map();
3564
+ const currentRenderMeta = /* @__PURE__ */ new Map();
3565
+ const renderFailures = /* @__PURE__ */ new Set();
3566
+ let completed = 0;
3567
+ const CONCURRENCY = 4;
3568
+ let nextIdx = 0;
3569
+ const renderOne = async (name) => {
3570
+ const descriptor = currentManifest.components[name];
3571
+ if (descriptor === void 0) return;
3572
+ const filePath = path.resolve(rootDir, descriptor.filePath);
3573
+ const outcome = await render.safeRender(
3574
+ () => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
3575
+ {
3576
+ props: {},
3577
+ sourceLocation: {
3578
+ file: descriptor.filePath,
3579
+ line: descriptor.loc.start,
3580
+ column: 0
3581
+ }
3582
+ }
3583
+ );
3584
+ completed++;
3585
+ const pct = Math.round(completed / total * 100);
3586
+ if (isTTY()) {
3587
+ process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
3588
+ }
3589
+ if (outcome.crashed) {
3590
+ renderFailures.add(name);
3591
+ return;
3592
+ }
3593
+ const result = outcome.result;
3594
+ currentRenderMeta.set(name, {
3595
+ width: result.width,
3596
+ height: result.height,
3597
+ renderTimeMs: result.renderTimeMs
3598
+ });
3599
+ computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
3600
+ };
3601
+ if (total > 0) {
3602
+ const worker = async () => {
3603
+ while (nextIdx < componentNames.length) {
3604
+ const i = nextIdx++;
3605
+ const name = componentNames[i];
3606
+ if (name !== void 0) {
3607
+ await renderOne(name);
3608
+ }
3609
+ }
3610
+ };
3611
+ const workers = [];
3612
+ for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
3613
+ workers.push(worker());
3614
+ }
3615
+ await Promise.all(workers);
3616
+ }
3617
+ await shutdownPool5();
3618
+ if (isTTY() && total > 0) {
3619
+ process.stderr.write("\n");
3620
+ }
3621
+ const resolver = new tokens.TokenResolver([]);
3622
+ const engine = new tokens.ComplianceEngine(resolver);
3623
+ const currentBatchReport = engine.auditBatch(computedStylesMap);
3624
+ const entries = [];
3625
+ for (const name of componentNames) {
3626
+ const baselineComp = baselineCompliance?.components[name] ?? null;
3627
+ const currentComp = currentBatchReport.components[name] ?? null;
3628
+ const baselineMeta = loadBaselineRenderJson(baselineDir, name);
3629
+ const currentMeta = currentRenderMeta.get(name) ?? null;
3630
+ const failed = renderFailures.has(name);
3631
+ const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
3632
+ const currentComplianceScore = currentComp?.compliance ?? null;
3633
+ const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
3634
+ const partial = {
3635
+ name,
3636
+ baselineCompliance: baselineComplianceScore,
3637
+ currentCompliance: currentComplianceScore,
3638
+ complianceDelta: delta,
3639
+ baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
3640
+ currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
3641
+ renderTimeMs: currentMeta?.renderTimeMs ?? null,
3642
+ renderFailed: failed
3643
+ };
3644
+ entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
3645
+ }
3646
+ for (const name of removedNames) {
3647
+ const baselineComp = baselineCompliance?.components[name] ?? null;
3648
+ const baselineMeta = loadBaselineRenderJson(baselineDir, name);
3649
+ entries.push({
3650
+ name,
3651
+ status: "removed",
3652
+ baselineCompliance: baselineComp?.aggregateCompliance ?? null,
3653
+ currentCompliance: null,
3654
+ complianceDelta: null,
3655
+ baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
3656
+ currentDimensions: null,
3657
+ renderTimeMs: null,
3658
+ renderFailed: false
3659
+ });
3660
+ }
3661
+ const summary = {
3662
+ total: entries.length,
3663
+ added: entries.filter((e) => e.status === "added").length,
3664
+ removed: entries.filter((e) => e.status === "removed").length,
3665
+ unchanged: entries.filter((e) => e.status === "unchanged").length,
3666
+ complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
3667
+ complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
3668
+ sizeChanged: entries.filter((e) => e.status === "size_changed").length,
3669
+ renderFailed: entries.filter((e) => e.renderFailed).length
3670
+ };
3671
+ const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
3672
+ const wallClockMs = performance.now() - startTime;
3673
+ return {
3674
+ diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
3675
+ baselineDir,
3676
+ summary,
3677
+ components: entries,
3678
+ baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
3679
+ currentAggregateCompliance: currentBatchReport.aggregateCompliance,
3680
+ hasRegressions,
3681
+ wallClockMs
3682
+ };
3683
+ }
3684
+ var STATUS_ICON = {
3685
+ added: "+",
3686
+ removed: "-",
3687
+ unchanged: " ",
3688
+ compliance_regressed: "\u2193",
3689
+ compliance_improved: "\u2191",
3690
+ size_changed: "~"
3691
+ };
3692
+ var STATUS_LABEL = {
3693
+ added: "added",
3694
+ removed: "removed",
3695
+ unchanged: "ok",
3696
+ compliance_regressed: "regressed",
3697
+ compliance_improved: "improved",
3698
+ size_changed: "size changed"
3699
+ };
3700
+ function formatDiffReport(result) {
3701
+ const lines = [];
3702
+ const title = "Scope Report Diff";
3703
+ const rule2 = "\u2501".repeat(Math.max(title.length, 40));
3704
+ lines.push(title, rule2);
3705
+ const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
3706
+ const complianceSign = complianceDelta >= 0 ? "+" : "";
3707
+ lines.push(
3708
+ `Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
3709
+ `Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
3710
+ `Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
3711
+ ""
3712
+ );
3713
+ const s = result.summary;
3714
+ lines.push(
3715
+ `Components: ${s.total} total ` + [
3716
+ s.added > 0 ? `${s.added} added` : "",
3717
+ s.removed > 0 ? `${s.removed} removed` : "",
3718
+ s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
3719
+ s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
3720
+ s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
3721
+ s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
3722
+ ].filter(Boolean).join(" "),
3723
+ ""
3724
+ );
3725
+ const notable = result.components.filter((e) => e.status !== "unchanged");
3726
+ if (notable.length === 0) {
3727
+ lines.push(" No changes detected.");
3728
+ } else {
3729
+ const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
3730
+ const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
3731
+ const divider = "-".repeat(header.length);
3732
+ lines.push(header, divider);
3733
+ for (const entry of notable) {
3734
+ const icon = STATUS_ICON[entry.status];
3735
+ const label = STATUS_LABEL[entry.status].padEnd(13);
3736
+ const name = entry.name.padEnd(nameWidth);
3737
+ let complianceStr = "\u2014".padEnd(13);
3738
+ if (entry.complianceDelta !== null) {
3739
+ const sign = entry.complianceDelta >= 0 ? "+" : "";
3740
+ complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
3741
+ }
3742
+ let dimStr = "\u2014";
3743
+ if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
3744
+ const b = entry.baselineDimensions;
3745
+ const c = entry.currentDimensions;
3746
+ if (b.width !== c.width || b.height !== c.height) {
3747
+ dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
3748
+ } else {
3749
+ dimStr = `${c.width}\xD7${c.height}`;
3750
+ }
3751
+ } else if (entry.currentDimensions !== null) {
3752
+ dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
3753
+ } else if (entry.baselineDimensions !== null) {
3754
+ dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
3755
+ }
3756
+ if (entry.renderFailed) {
3757
+ dimStr = "render failed";
3758
+ }
3759
+ lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
3760
+ }
3761
+ }
3762
+ lines.push(
3763
+ "",
3764
+ rule2,
3765
+ result.hasRegressions ? `Diff complete: ${result.summary.complianceRegressed + result.summary.renderFailed} regression(s) detected in ${(result.wallClockMs / 1e3).toFixed(1)}s` : `Diff complete: no regressions in ${(result.wallClockMs / 1e3).toFixed(1)}s`
3766
+ );
3767
+ return lines.join("\n");
3768
+ }
3769
+ function registerDiffSubCommand(reportCmd) {
3770
+ reportCmd.command("diff").description("Compare the current component library against a saved baseline snapshot").option("-b, --baseline <dir>", "Baseline directory to compare against", DEFAULT_BASELINE_DIR2).option("--components <glob>", "Glob pattern to diff a subset of components").option("--manifest <path>", "Path to an existing manifest.json to use instead of regenerating").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").option("--json", "Output diff as JSON instead of human-readable text", false).option("-o, --output <path>", "Write the diff JSON to a file").option(
3771
+ "--regression-threshold <n>",
3772
+ "Minimum compliance drop (0\u20131) to classify as a regression",
3773
+ "0.01"
3774
+ ).action(
3775
+ async (opts) => {
3776
+ try {
3777
+ const [wStr, hStr] = opts.viewport.split("x");
3778
+ const viewportWidth = Number.parseInt(wStr ?? "375", 10);
3779
+ const viewportHeight = Number.parseInt(hStr ?? "812", 10);
3780
+ const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
3781
+ const result = await runDiff({
3782
+ baselineDir: opts.baseline,
3783
+ componentsGlob: opts.components,
3784
+ manifestPath: opts.manifest,
3785
+ viewportWidth,
3786
+ viewportHeight,
3787
+ regressionThreshold
3788
+ });
3789
+ if (opts.output !== void 0) {
3790
+ fs.writeFileSync(opts.output, JSON.stringify(result, null, 2), "utf-8");
3791
+ process.stderr.write(`Diff written to ${opts.output}
3792
+ `);
3793
+ }
3794
+ if (opts.json) {
3795
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
3796
+ `);
3797
+ } else {
3798
+ process.stdout.write(`${formatDiffReport(result)}
3799
+ `);
3800
+ }
3801
+ process.exit(result.hasRegressions ? 1 : 0);
3802
+ } catch (err) {
3803
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3804
+ `);
3805
+ process.exit(2);
3806
+ }
3807
+ }
3808
+ );
3809
+ }
3335
3810
 
3336
3811
  // src/tree-formatter.ts
3337
3812
  var BRANCH2 = "\u251C\u2500\u2500 ";
@@ -3610,10 +4085,178 @@ function buildStructuredReport(report) {
3610
4085
  route: report.route?.pattern ?? null
3611
4086
  };
3612
4087
  }
4088
+ var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
4089
+ function loadStylesFile(stylesPath) {
4090
+ const absPath = path.resolve(process.cwd(), stylesPath);
4091
+ if (!fs.existsSync(absPath)) {
4092
+ throw new Error(
4093
+ `Compliance styles file not found at ${absPath}.
4094
+ Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
4095
+ Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
4096
+ );
4097
+ }
4098
+ const raw = fs.readFileSync(absPath, "utf-8");
4099
+ let parsed;
4100
+ try {
4101
+ parsed = JSON.parse(raw);
4102
+ } catch (err) {
4103
+ throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
4104
+ }
4105
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4106
+ throw new Error(
4107
+ `Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
4108
+ );
4109
+ }
4110
+ return parsed;
4111
+ }
4112
+ function categoryForProperty(property) {
4113
+ const lower = property.toLowerCase();
4114
+ if (lower.includes("shadow")) return "shadow";
4115
+ if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
4116
+ return "color";
4117
+ if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
4118
+ return "spacing";
4119
+ if (lower.includes("border")) return "border";
4120
+ if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
4121
+ return "typography";
4122
+ return "spacing";
4123
+ }
4124
+ function buildCategorySummary(batch) {
4125
+ const cats = {
4126
+ color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4127
+ spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4128
+ typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4129
+ border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4130
+ shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
4131
+ };
4132
+ for (const report of Object.values(batch.components)) {
4133
+ for (const [property, result] of Object.entries(report.properties)) {
4134
+ const cat = categoryForProperty(property);
4135
+ const summary = cats[cat];
4136
+ if (summary === void 0) continue;
4137
+ summary.total++;
4138
+ if (result.status === "on_system") {
4139
+ summary.onSystem++;
4140
+ } else {
4141
+ summary.offSystem++;
4142
+ }
4143
+ }
4144
+ }
4145
+ for (const summary of Object.values(cats)) {
4146
+ summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
4147
+ }
4148
+ return cats;
4149
+ }
4150
+ function collectOffenders(batch, limit = 10) {
4151
+ const offenders = [];
4152
+ const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
4153
+ name,
4154
+ report,
4155
+ offSystemCount: report.offSystem
4156
+ }));
4157
+ componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
4158
+ for (const { name, report, offSystemCount } of componentEntries) {
4159
+ if (offSystemCount === 0) continue;
4160
+ for (const [property, result] of Object.entries(report.properties)) {
4161
+ if (result.status !== "OFF_SYSTEM") continue;
4162
+ offenders.push({
4163
+ component: name,
4164
+ property,
4165
+ value: result.value,
4166
+ nearestToken: result.nearest?.token ?? "\u2014",
4167
+ nearestValue: result.nearest?.value ?? "\u2014",
4168
+ offSystemCount
4169
+ });
4170
+ if (offenders.length >= limit) break;
4171
+ }
4172
+ if (offenders.length >= limit) break;
4173
+ }
4174
+ return offenders;
4175
+ }
4176
+ function formatPct(n) {
4177
+ return `${Math.round(n * 100)}%`;
4178
+ }
4179
+ function truncate(s, max) {
4180
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
4181
+ }
4182
+ function formatComplianceReport(batch, threshold) {
4183
+ const pct = Math.round(batch.aggregateCompliance * 100);
4184
+ const lines = [];
4185
+ const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
4186
+ lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
4187
+ lines.push("");
4188
+ const cats = buildCategorySummary(batch);
4189
+ const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
4190
+ if (catEntries.length > 0) {
4191
+ lines.push("By category:");
4192
+ const catWidth = Math.max(...catEntries.map(([k]) => k.length));
4193
+ for (const [cat, summary] of catEntries) {
4194
+ const label = cat.padEnd(catWidth);
4195
+ lines.push(
4196
+ ` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
4197
+ );
4198
+ }
4199
+ lines.push("");
4200
+ }
4201
+ const offenders = collectOffenders(batch);
4202
+ if (offenders.length > 0) {
4203
+ lines.push("Top off-system offenders (sorted by count):");
4204
+ const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
4205
+ const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
4206
+ const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
4207
+ for (const offender of offenders) {
4208
+ const name = offender.component.padEnd(nameWidth);
4209
+ const prop = offender.property.padEnd(propWidth);
4210
+ const val = truncate(offender.value, 40).padEnd(valWidth);
4211
+ const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
4212
+ lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
4213
+ }
4214
+ } else {
4215
+ lines.push("No off-system values detected. \u{1F389}");
4216
+ }
4217
+ return lines.join("\n");
4218
+ }
4219
+ function registerCompliance(tokensCmd) {
4220
+ tokensCmd.command("compliance").description("Aggregate token compliance report across all components (Token Spec \xA73.3 format)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH})`).option("--threshold <n>", "Exit code 1 if compliance score is below this percentage (0-100)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
4221
+ try {
4222
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4223
+ const { tokens: tokens$1 } = loadTokens(tokenFilePath);
4224
+ const resolver = new tokens.TokenResolver(tokens$1);
4225
+ const engine = new tokens.ComplianceEngine(resolver);
4226
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
4227
+ const stylesFile = loadStylesFile(stylesPath);
4228
+ const componentMap = /* @__PURE__ */ new Map();
4229
+ for (const [name, styles] of Object.entries(stylesFile)) {
4230
+ componentMap.set(name, styles);
4231
+ }
4232
+ if (componentMap.size === 0) {
4233
+ process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
4234
+ `);
4235
+ }
4236
+ const batch = engine.auditBatch(componentMap);
4237
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4238
+ const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
4239
+ if (useJson) {
4240
+ process.stdout.write(`${JSON.stringify(batch, null, 2)}
4241
+ `);
4242
+ } else {
4243
+ process.stdout.write(`${formatComplianceReport(batch, threshold)}
4244
+ `);
4245
+ }
4246
+ if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
4247
+ process.exit(1);
4248
+ }
4249
+ } catch (err) {
4250
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4251
+ `);
4252
+ process.exit(1);
4253
+ }
4254
+ });
4255
+ }
3613
4256
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
3614
4257
  var CONFIG_FILE = "reactscope.config.json";
3615
4258
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
3616
- function resolveTokenFilePath(fileFlag) {
4259
+ function resolveTokenFilePath2(fileFlag) {
3617
4260
  if (fileFlag !== void 0) {
3618
4261
  return path.resolve(process.cwd(), fileFlag);
3619
4262
  }
@@ -3647,7 +4290,7 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
3647
4290
  }
3648
4291
  const format = opts.format;
3649
4292
  try {
3650
- const filePath = resolveTokenFilePath(opts.file);
4293
+ const filePath = resolveTokenFilePath2(opts.file);
3651
4294
  if (!fs.existsSync(filePath)) {
3652
4295
  throw new Error(
3653
4296
  `Token file not found at ${filePath}.
@@ -3711,6 +4354,311 @@ Available themes: ${themeNames.join(", ")}`
3711
4354
  }
3712
4355
  );
3713
4356
  }
4357
+ var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
4358
+ var SEVERITY_EMOJI = {
4359
+ none: "\u25CB",
4360
+ subtle: "\u25D4",
4361
+ moderate: "\u25D1",
4362
+ significant: "\u25CF"
4363
+ };
4364
+ function formatImpactReport(report) {
4365
+ const lines = [];
4366
+ const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
4367
+ lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
4368
+ if (report.components.length === 0) {
4369
+ lines.push("");
4370
+ lines.push("No components reference this token.");
4371
+ return lines.join("\n");
4372
+ }
4373
+ lines.push("");
4374
+ const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
4375
+ const propWidth = Math.max(
4376
+ 8,
4377
+ ...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
4378
+ );
4379
+ for (const comp of report.components) {
4380
+ for (const property of comp.affectedProperties) {
4381
+ const name = comp.name.padEnd(nameWidth);
4382
+ const prop = property.padEnd(propWidth);
4383
+ const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
4384
+ lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
4385
+ }
4386
+ }
4387
+ lines.push("");
4388
+ const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
4389
+ const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
4390
+ lines.push(
4391
+ `${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
4392
+ );
4393
+ if (report.colorDelta !== void 0) {
4394
+ lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
4395
+ }
4396
+ return lines.join("\n");
4397
+ }
4398
+ function formatImpactSummary(report) {
4399
+ if (report.components.length === 0) {
4400
+ return `No components reference token "${report.tokenPath}".`;
4401
+ }
4402
+ const parts = report.components.map(
4403
+ (c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
4404
+ );
4405
+ return `\u2192 ${parts.join(", ")}`;
4406
+ }
4407
+ function registerImpact(tokensCmd) {
4408
+ tokensCmd.command("impact <path>").description("List all components and elements that consume a given token (Token Spec \xA74.3)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH2})`).option("--new-value <value>", "Proposed new value \u2014 report visual severity of the change").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action(
4409
+ (tokenPath, opts) => {
4410
+ try {
4411
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4412
+ const { tokens: tokens$1 } = loadTokens(tokenFilePath);
4413
+ const resolver = new tokens.TokenResolver(tokens$1);
4414
+ const engine = new tokens.ComplianceEngine(resolver);
4415
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
4416
+ const stylesFile = loadStylesFile(stylesPath);
4417
+ const componentMap = new Map(Object.entries(stylesFile));
4418
+ const batchReport = engine.auditBatch(componentMap);
4419
+ const complianceReports = new Map(Object.entries(batchReport.components));
4420
+ const analyzer = new tokens.ImpactAnalyzer(resolver, complianceReports);
4421
+ const currentValue = resolver.resolve(tokenPath);
4422
+ const newValue = opts.newValue ?? currentValue;
4423
+ const report = analyzer.impactOf(tokenPath, newValue);
4424
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4425
+ if (useJson) {
4426
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
4427
+ `);
4428
+ } else {
4429
+ process.stdout.write(`${formatImpactReport(report)}
4430
+ `);
4431
+ if (isTTY()) {
4432
+ process.stdout.write(`
4433
+ ${formatImpactSummary(report)}
4434
+ `);
4435
+ }
4436
+ }
4437
+ } catch (err) {
4438
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4439
+ `);
4440
+ process.exit(1);
4441
+ }
4442
+ }
4443
+ );
4444
+ }
4445
+ var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
4446
+ var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
4447
+ var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
4448
+ async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
4449
+ const htmlHarness = await buildComponentHarness(
4450
+ filePath,
4451
+ componentName,
4452
+ {},
4453
+ // no props
4454
+ vpWidth,
4455
+ cssOverride
4456
+ // injected as <style>
4457
+ );
4458
+ const pool = new render.BrowserPool({
4459
+ size: { browsers: 1, pagesPerBrowser: 1 },
4460
+ viewportWidth: vpWidth,
4461
+ viewportHeight: vpHeight
4462
+ });
4463
+ await pool.init();
4464
+ const slot = await pool.acquire();
4465
+ const { page } = slot;
4466
+ try {
4467
+ await page.setContent(htmlHarness, { waitUntil: "load" });
4468
+ await page.waitForFunction(
4469
+ () => {
4470
+ const w = window;
4471
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
4472
+ },
4473
+ { timeout: timeoutMs }
4474
+ );
4475
+ const rootLocator = page.locator("[data-reactscope-root]");
4476
+ const bb = await rootLocator.boundingBox();
4477
+ const PAD = 16;
4478
+ const MIN_W = 320;
4479
+ const MIN_H = 120;
4480
+ const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
4481
+ const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
4482
+ const rawW = (bb?.width ?? MIN_W) + PAD * 2;
4483
+ const rawH = (bb?.height ?? MIN_H) + PAD * 2;
4484
+ const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
4485
+ const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
4486
+ const screenshot = await page.screenshot({
4487
+ clip: { x: clipX, y: clipY, width: clipW, height: clipH },
4488
+ type: "png"
4489
+ });
4490
+ return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
4491
+ } finally {
4492
+ pool.release(slot);
4493
+ await pool.close().catch(() => void 0);
4494
+ }
4495
+ }
4496
+ function registerPreview(tokensCmd) {
4497
+ tokensCmd.command("preview <path>").description("Render before/after sprite sheet for components affected by a token change").requiredOption("--new-value <value>", "The proposed new resolved value for the token").option("--sprite", "Output a PNG sprite sheet (default when TTY)", false).option("-o, --output <path>", "Output PNG path (default: .reactscope/previews/<token>.png)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH3})`).option("--manifest <path>", "Path to manifest.json", DEFAULT_MANIFEST_PATH).option("--format <fmt>", "Output format: json or text (default: auto-detect)").option("--timeout <ms>", "Browser timeout per render (ms)", "10000").option("--viewport-width <px>", "Viewport width in pixels", "1280").option("--viewport-height <px>", "Viewport height in pixels", "720").action(
4498
+ async (tokenPath, opts) => {
4499
+ try {
4500
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4501
+ const { tokens: tokens$1 } = loadTokens(tokenFilePath);
4502
+ const resolver = new tokens.TokenResolver(tokens$1);
4503
+ const engine = new tokens.ComplianceEngine(resolver);
4504
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
4505
+ const stylesFile = loadStylesFile(stylesPath);
4506
+ const componentMap = new Map(Object.entries(stylesFile));
4507
+ const batchReport = engine.auditBatch(componentMap);
4508
+ const complianceReports = new Map(Object.entries(batchReport.components));
4509
+ const analyzer = new tokens.ImpactAnalyzer(resolver, complianceReports);
4510
+ const currentValue = resolver.resolve(tokenPath);
4511
+ const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
4512
+ if (impactReport.components.length === 0) {
4513
+ process.stdout.write(
4514
+ `No components reference token "${tokenPath}". Nothing to preview.
4515
+ `
4516
+ );
4517
+ return;
4518
+ }
4519
+ const affectedNames = impactReport.components.map((c) => c.name);
4520
+ process.stderr.write(
4521
+ `Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
4522
+ `
4523
+ );
4524
+ const manifest = loadManifest(opts.manifest);
4525
+ const vpWidth = Number.parseInt(opts.viewportWidth, 10);
4526
+ const vpHeight = Number.parseInt(opts.viewportHeight, 10);
4527
+ const timeout = Number.parseInt(opts.timeout, 10);
4528
+ const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
4529
+ const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
4530
+ const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
4531
+ const renders = [];
4532
+ for (const componentName of affectedNames) {
4533
+ const descriptor = manifest.components[componentName];
4534
+ if (descriptor === void 0) {
4535
+ process.stderr.write(
4536
+ `Warning: "${componentName}" not found in manifest \u2014 skipping
4537
+ `
4538
+ );
4539
+ continue;
4540
+ }
4541
+ process.stderr.write(` Rendering ${componentName} (before)...
4542
+ `);
4543
+ const before = await renderComponentWithCssOverride(
4544
+ descriptor.filePath,
4545
+ componentName,
4546
+ beforeCss,
4547
+ vpWidth,
4548
+ vpHeight,
4549
+ timeout
4550
+ );
4551
+ process.stderr.write(` Rendering ${componentName} (after)...
4552
+ `);
4553
+ const after = await renderComponentWithCssOverride(
4554
+ descriptor.filePath,
4555
+ componentName,
4556
+ afterCss,
4557
+ vpWidth,
4558
+ vpHeight,
4559
+ timeout
4560
+ );
4561
+ renders.push({ name: componentName, before, after });
4562
+ }
4563
+ if (renders.length === 0) {
4564
+ process.stderr.write(
4565
+ "Warning: No components could be rendered (all missing from manifest).\n"
4566
+ );
4567
+ return;
4568
+ }
4569
+ const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
4570
+ const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
4571
+ const cells = renders.flatMap((r, colIdx) => [
4572
+ {
4573
+ props: { version: "before", component: r.name },
4574
+ result: {
4575
+ screenshot: r.before.screenshot,
4576
+ width: cellW,
4577
+ height: cellH,
4578
+ renderTimeMs: 0,
4579
+ computedStyles: {}
4580
+ },
4581
+ index: colIdx * 2,
4582
+ axisIndices: [0, colIdx]
4583
+ },
4584
+ {
4585
+ props: { version: "after", component: r.name },
4586
+ result: {
4587
+ screenshot: r.after.screenshot,
4588
+ width: cellW,
4589
+ height: cellH,
4590
+ renderTimeMs: 0,
4591
+ computedStyles: {}
4592
+ },
4593
+ index: colIdx * 2 + 1,
4594
+ axisIndices: [1, colIdx]
4595
+ }
4596
+ ]);
4597
+ const matrixResult = {
4598
+ cells,
4599
+ axes: [
4600
+ { name: "component", values: renders.map((r) => r.name) },
4601
+ { name: "version", values: ["before", "after"] }
4602
+ ],
4603
+ axisLabels: [renders.map((r) => r.name), ["before", "after"]],
4604
+ rows: 2,
4605
+ cols: renders.length,
4606
+ stats: {
4607
+ totalCells: cells.length,
4608
+ totalRenderTimeMs: 0,
4609
+ avgRenderTimeMs: 0,
4610
+ minRenderTimeMs: 0,
4611
+ maxRenderTimeMs: 0,
4612
+ wallClockTimeMs: 0
4613
+ }
4614
+ };
4615
+ const generator = new render.SpriteSheetGenerator({
4616
+ cellPadding: 8,
4617
+ borderWidth: 1,
4618
+ labelHeight: 32,
4619
+ labelWidth: 120
4620
+ });
4621
+ const spriteResult = await generator.generate(matrixResult);
4622
+ const tokenLabel = tokenPath.replace(/\./g, "-");
4623
+ const outputPath = opts.output ?? path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
4624
+ const outputDir = path.resolve(outputPath, "..");
4625
+ fs.mkdirSync(outputDir, { recursive: true });
4626
+ fs.writeFileSync(outputPath, spriteResult.png);
4627
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4628
+ if (useJson) {
4629
+ process.stdout.write(
4630
+ `${JSON.stringify(
4631
+ {
4632
+ tokenPath,
4633
+ oldValue: currentValue,
4634
+ newValue: opts.newValue,
4635
+ outputPath,
4636
+ width: spriteResult.width,
4637
+ height: spriteResult.height,
4638
+ components: renders.map((r) => r.name),
4639
+ cells: spriteResult.coordinates.length
4640
+ },
4641
+ null,
4642
+ 2
4643
+ )}
4644
+ `
4645
+ );
4646
+ } else {
4647
+ process.stdout.write(
4648
+ `Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
4649
+ `
4650
+ );
4651
+ process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
4652
+ `);
4653
+ }
4654
+ } catch (err) {
4655
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4656
+ `);
4657
+ process.exit(1);
4658
+ }
4659
+ }
4660
+ );
4661
+ }
3714
4662
 
3715
4663
  // src/tokens/commands.ts
3716
4664
  var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
@@ -3732,7 +4680,7 @@ function buildTable2(headers, rows) {
3732
4680
  );
3733
4681
  return [headerRow, divider, ...dataRows].join("\n");
3734
4682
  }
3735
- function resolveTokenFilePath2(fileFlag) {
4683
+ function resolveTokenFilePath(fileFlag) {
3736
4684
  if (fileFlag !== void 0) {
3737
4685
  return path.resolve(process.cwd(), fileFlag);
3738
4686
  }
@@ -3795,7 +4743,7 @@ function buildResolutionChain(startPath, rawTokens) {
3795
4743
  function registerGet2(tokensCmd) {
3796
4744
  tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
3797
4745
  try {
3798
- const filePath = resolveTokenFilePath2(opts.file);
4746
+ const filePath = resolveTokenFilePath(opts.file);
3799
4747
  const { tokens: tokens$1 } = loadTokens(filePath);
3800
4748
  const resolver = new tokens.TokenResolver(tokens$1);
3801
4749
  const resolvedValue = resolver.resolve(tokenPath);
@@ -3821,7 +4769,7 @@ function registerList2(tokensCmd) {
3821
4769
  tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
3822
4770
  (category, opts) => {
3823
4771
  try {
3824
- const filePath = resolveTokenFilePath2(opts.file);
4772
+ const filePath = resolveTokenFilePath(opts.file);
3825
4773
  const { tokens: tokens$1 } = loadTokens(filePath);
3826
4774
  const resolver = new tokens.TokenResolver(tokens$1);
3827
4775
  const filtered = resolver.list(opts.type, category);
@@ -3851,7 +4799,7 @@ function registerSearch(tokensCmd) {
3851
4799
  tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
3852
4800
  (value, opts) => {
3853
4801
  try {
3854
- const filePath = resolveTokenFilePath2(opts.file);
4802
+ const filePath = resolveTokenFilePath(opts.file);
3855
4803
  const { tokens: tokens$1 } = loadTokens(filePath);
3856
4804
  const resolver = new tokens.TokenResolver(tokens$1);
3857
4805
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
@@ -3933,7 +4881,7 @@ Tip: use --fuzzy for nearest-match search.
3933
4881
  function registerResolve(tokensCmd) {
3934
4882
  tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
3935
4883
  try {
3936
- const filePath = resolveTokenFilePath2(opts.file);
4884
+ const filePath = resolveTokenFilePath(opts.file);
3937
4885
  const absFilePath = filePath;
3938
4886
  const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
3939
4887
  const resolver = new tokens.TokenResolver(tokens$1);
@@ -3970,7 +4918,7 @@ function registerValidate(tokensCmd) {
3970
4918
  "Validate the token file for errors (circular refs, missing refs, type mismatches)"
3971
4919
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
3972
4920
  try {
3973
- const filePath = resolveTokenFilePath2(opts.file);
4921
+ const filePath = resolveTokenFilePath(opts.file);
3974
4922
  if (!fs.existsSync(filePath)) {
3975
4923
  throw new Error(
3976
4924
  `Token file not found at ${filePath}.
@@ -4054,6 +5002,9 @@ function createTokensCommand() {
4054
5002
  registerResolve(tokensCmd);
4055
5003
  registerValidate(tokensCmd);
4056
5004
  tokensCmd.addCommand(createTokensExportCommand());
5005
+ registerCompliance(tokensCmd);
5006
+ registerImpact(tokensCmd);
5007
+ registerPreview(tokensCmd);
4057
5008
  return tokensCmd;
4058
5009
  }
4059
5010
 
@@ -4149,6 +5100,7 @@ function createProgram(options = {}) {
4149
5100
  const existingReportCmd = program.commands.find((c) => c.name() === "report");
4150
5101
  if (existingReportCmd !== void 0) {
4151
5102
  registerBaselineSubCommand(existingReportCmd);
5103
+ registerDiffSubCommand(existingReportCmd);
4152
5104
  }
4153
5105
  return program;
4154
5106
  }
@@ -4161,7 +5113,7 @@ exports.createTokensCommand = createTokensCommand;
4161
5113
  exports.createTokensExportCommand = createTokensExportCommand;
4162
5114
  exports.isTTY = isTTY;
4163
5115
  exports.matchGlob = matchGlob;
4164
- exports.resolveTokenFilePath = resolveTokenFilePath;
5116
+ exports.resolveTokenFilePath = resolveTokenFilePath2;
4165
5117
  exports.runInit = runInit;
4166
5118
  //# sourceMappingURL=index.cjs.map
4167
5119
  //# sourceMappingURL=index.cjs.map