@agent-scope/cli 1.12.0 → 1.13.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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync7 } from "fs";
4
+ import { readFileSync as readFileSync8 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
6
  import { Command as Command8 } from "commander";
7
7
 
@@ -255,9 +255,9 @@ function createRL() {
255
255
  });
256
256
  }
257
257
  async function ask(rl, question) {
258
- return new Promise((resolve11) => {
258
+ return new Promise((resolve12) => {
259
259
  rl.question(question, (answer) => {
260
- resolve11(answer.trim());
260
+ resolve12(answer.trim());
261
261
  });
262
262
  });
263
263
  }
@@ -3217,12 +3217,12 @@ async function runBaseline(options = {}) {
3217
3217
  mkdirSync4(rendersDir, { recursive: true });
3218
3218
  let manifest;
3219
3219
  if (manifestPath !== void 0) {
3220
- const { readFileSync: readFileSync8 } = await import("fs");
3220
+ const { readFileSync: readFileSync9 } = await import("fs");
3221
3221
  const absPath = resolve8(rootDir, manifestPath);
3222
3222
  if (!existsSync5(absPath)) {
3223
3223
  throw new Error(`Manifest not found at ${absPath}.`);
3224
3224
  }
3225
- manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
3225
+ manifest = JSON.parse(readFileSync9(absPath, "utf-8"));
3226
3226
  process.stderr.write(`Loaded manifest from ${manifestPath}
3227
3227
  `);
3228
3228
  } else {
@@ -3384,6 +3384,486 @@ function registerBaselineSubCommand(reportCmd) {
3384
3384
  );
3385
3385
  }
3386
3386
 
3387
+ // src/report/diff.ts
3388
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3389
+ import { resolve as resolve9 } from "path";
3390
+ import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
3391
+ import { BrowserPool as BrowserPool5, safeRender as safeRender3 } from "@agent-scope/render";
3392
+ import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
3393
+ var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
3394
+ function loadBaselineCompliance(baselineDir) {
3395
+ const compliancePath = resolve9(baselineDir, "compliance.json");
3396
+ if (!existsSync6(compliancePath)) return null;
3397
+ const raw = JSON.parse(readFileSync5(compliancePath, "utf-8"));
3398
+ return raw;
3399
+ }
3400
+ function loadBaselineRenderJson(baselineDir, componentName) {
3401
+ const jsonPath = resolve9(baselineDir, "renders", `${componentName}.json`);
3402
+ if (!existsSync6(jsonPath)) return null;
3403
+ return JSON.parse(readFileSync5(jsonPath, "utf-8"));
3404
+ }
3405
+ var _pool5 = null;
3406
+ async function getPool5(viewportWidth, viewportHeight) {
3407
+ if (_pool5 === null) {
3408
+ _pool5 = new BrowserPool5({
3409
+ size: { browsers: 1, pagesPerBrowser: 4 },
3410
+ viewportWidth,
3411
+ viewportHeight
3412
+ });
3413
+ await _pool5.init();
3414
+ }
3415
+ return _pool5;
3416
+ }
3417
+ async function shutdownPool5() {
3418
+ if (_pool5 !== null) {
3419
+ await _pool5.close();
3420
+ _pool5 = null;
3421
+ }
3422
+ }
3423
+ async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
3424
+ const pool = await getPool5(viewportWidth, viewportHeight);
3425
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
3426
+ const slot = await pool.acquire();
3427
+ const { page } = slot;
3428
+ try {
3429
+ await page.setContent(htmlHarness, { waitUntil: "load" });
3430
+ await page.waitForFunction(
3431
+ () => {
3432
+ const w = window;
3433
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
3434
+ },
3435
+ { timeout: 15e3 }
3436
+ );
3437
+ const renderError = await page.evaluate(() => {
3438
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
3439
+ });
3440
+ if (renderError !== null) {
3441
+ throw new Error(`Component render error: ${renderError}`);
3442
+ }
3443
+ const rootDir = process.cwd();
3444
+ const classes = await page.evaluate(() => {
3445
+ const set = /* @__PURE__ */ new Set();
3446
+ document.querySelectorAll("[class]").forEach((el) => {
3447
+ for (const c of el.className.split(/\s+/)) {
3448
+ if (c) set.add(c);
3449
+ }
3450
+ });
3451
+ return [...set];
3452
+ });
3453
+ const projectCss = await getCompiledCssForClasses(rootDir, classes);
3454
+ if (projectCss != null && projectCss.length > 0) {
3455
+ await page.addStyleTag({ content: projectCss });
3456
+ }
3457
+ const startMs = performance.now();
3458
+ const rootLocator = page.locator("[data-reactscope-root]");
3459
+ const boundingBox = await rootLocator.boundingBox();
3460
+ if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
3461
+ throw new Error(
3462
+ `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3463
+ );
3464
+ }
3465
+ const PAD = 24;
3466
+ const MIN_W = 320;
3467
+ const MIN_H = 200;
3468
+ const clipX = Math.max(0, boundingBox.x - PAD);
3469
+ const clipY = Math.max(0, boundingBox.y - PAD);
3470
+ const rawW = boundingBox.width + PAD * 2;
3471
+ const rawH = boundingBox.height + PAD * 2;
3472
+ const clipW = Math.max(rawW, MIN_W);
3473
+ const clipH = Math.max(rawH, MIN_H);
3474
+ const safeW = Math.min(clipW, viewportWidth - clipX);
3475
+ const safeH = Math.min(clipH, viewportHeight - clipY);
3476
+ const screenshot = await page.screenshot({
3477
+ clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3478
+ type: "png"
3479
+ });
3480
+ const computedStylesRaw = {};
3481
+ const styles = await page.evaluate((sel) => {
3482
+ const el = document.querySelector(sel);
3483
+ if (el === null) return {};
3484
+ const computed = window.getComputedStyle(el);
3485
+ const out = {};
3486
+ for (const prop of [
3487
+ "display",
3488
+ "width",
3489
+ "height",
3490
+ "color",
3491
+ "backgroundColor",
3492
+ "fontSize",
3493
+ "fontFamily",
3494
+ "padding",
3495
+ "margin"
3496
+ ]) {
3497
+ out[prop] = computed.getPropertyValue(prop);
3498
+ }
3499
+ return out;
3500
+ }, "[data-reactscope-root] > *");
3501
+ computedStylesRaw["[data-reactscope-root] > *"] = styles;
3502
+ const renderTimeMs = performance.now() - startMs;
3503
+ return {
3504
+ screenshot,
3505
+ width: Math.round(safeW),
3506
+ height: Math.round(safeH),
3507
+ renderTimeMs,
3508
+ computedStyles: computedStylesRaw
3509
+ };
3510
+ } finally {
3511
+ pool.release(slot);
3512
+ }
3513
+ }
3514
+ function extractComputedStyles2(computedStylesRaw) {
3515
+ const flat = {};
3516
+ for (const styles of Object.values(computedStylesRaw)) {
3517
+ Object.assign(flat, styles);
3518
+ }
3519
+ const colors = {};
3520
+ const spacing = {};
3521
+ const typography = {};
3522
+ const borders = {};
3523
+ const shadows = {};
3524
+ for (const [prop, value] of Object.entries(flat)) {
3525
+ if (prop === "color" || prop === "backgroundColor") {
3526
+ colors[prop] = value;
3527
+ } else if (prop === "padding" || prop === "margin") {
3528
+ spacing[prop] = value;
3529
+ } else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
3530
+ typography[prop] = value;
3531
+ } else if (prop === "borderRadius" || prop === "borderWidth") {
3532
+ borders[prop] = value;
3533
+ } else if (prop === "boxShadow") {
3534
+ shadows[prop] = value;
3535
+ }
3536
+ }
3537
+ return { colors, spacing, typography, borders, shadows };
3538
+ }
3539
+ function classifyComponent(entry, regressionThreshold) {
3540
+ if (entry.renderFailed) return "unchanged";
3541
+ if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
3542
+ return "added";
3543
+ }
3544
+ if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
3545
+ return "removed";
3546
+ }
3547
+ const delta = entry.complianceDelta;
3548
+ if (delta !== null) {
3549
+ if (delta <= -regressionThreshold) return "compliance_regressed";
3550
+ if (delta >= regressionThreshold) return "compliance_improved";
3551
+ }
3552
+ if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
3553
+ const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
3554
+ const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
3555
+ if (dw > 10 || dh > 10) return "size_changed";
3556
+ }
3557
+ return "unchanged";
3558
+ }
3559
+ async function runDiff(options = {}) {
3560
+ const {
3561
+ baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
3562
+ componentsGlob,
3563
+ manifestPath,
3564
+ viewportWidth = 375,
3565
+ viewportHeight = 812,
3566
+ regressionThreshold = 0.01
3567
+ } = options;
3568
+ const startTime = performance.now();
3569
+ const rootDir = process.cwd();
3570
+ const baselineDir = resolve9(rootDir, baselineDirRaw);
3571
+ if (!existsSync6(baselineDir)) {
3572
+ throw new Error(
3573
+ `Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
3574
+ );
3575
+ }
3576
+ const baselineManifestPath = resolve9(baselineDir, "manifest.json");
3577
+ if (!existsSync6(baselineManifestPath)) {
3578
+ throw new Error(
3579
+ `Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
3580
+ );
3581
+ }
3582
+ const baselineManifest = JSON.parse(readFileSync5(baselineManifestPath, "utf-8"));
3583
+ const baselineCompliance = loadBaselineCompliance(baselineDir);
3584
+ const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
3585
+ process.stderr.write(
3586
+ `Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
3587
+ `
3588
+ );
3589
+ let currentManifest;
3590
+ if (manifestPath !== void 0) {
3591
+ const absPath = resolve9(rootDir, manifestPath);
3592
+ if (!existsSync6(absPath)) {
3593
+ throw new Error(`Manifest not found at "${absPath}".`);
3594
+ }
3595
+ currentManifest = JSON.parse(readFileSync5(absPath, "utf-8"));
3596
+ process.stderr.write(`Loaded manifest from ${manifestPath}
3597
+ `);
3598
+ } else {
3599
+ process.stderr.write("Scanning for React components\u2026\n");
3600
+ currentManifest = await generateManifest3({ rootDir });
3601
+ const count = Object.keys(currentManifest.components).length;
3602
+ process.stderr.write(`Found ${count} components.
3603
+ `);
3604
+ }
3605
+ let componentNames = Object.keys(currentManifest.components);
3606
+ if (componentsGlob !== void 0) {
3607
+ componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
3608
+ process.stderr.write(
3609
+ `Filtered to ${componentNames.length} components matching "${componentsGlob}".
3610
+ `
3611
+ );
3612
+ }
3613
+ const removedNames = [...baselineComponentNames].filter(
3614
+ (name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
3615
+ );
3616
+ const total = componentNames.length;
3617
+ process.stderr.write(`Rendering ${total} components for diff\u2026
3618
+ `);
3619
+ const computedStylesMap = /* @__PURE__ */ new Map();
3620
+ const currentRenderMeta = /* @__PURE__ */ new Map();
3621
+ const renderFailures = /* @__PURE__ */ new Set();
3622
+ let completed = 0;
3623
+ const CONCURRENCY = 4;
3624
+ let nextIdx = 0;
3625
+ const renderOne = async (name) => {
3626
+ const descriptor = currentManifest.components[name];
3627
+ if (descriptor === void 0) return;
3628
+ const filePath = resolve9(rootDir, descriptor.filePath);
3629
+ const outcome = await safeRender3(
3630
+ () => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
3631
+ {
3632
+ props: {},
3633
+ sourceLocation: {
3634
+ file: descriptor.filePath,
3635
+ line: descriptor.loc.start,
3636
+ column: 0
3637
+ }
3638
+ }
3639
+ );
3640
+ completed++;
3641
+ const pct = Math.round(completed / total * 100);
3642
+ if (isTTY()) {
3643
+ process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
3644
+ }
3645
+ if (outcome.crashed) {
3646
+ renderFailures.add(name);
3647
+ return;
3648
+ }
3649
+ const result = outcome.result;
3650
+ currentRenderMeta.set(name, {
3651
+ width: result.width,
3652
+ height: result.height,
3653
+ renderTimeMs: result.renderTimeMs
3654
+ });
3655
+ computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
3656
+ };
3657
+ if (total > 0) {
3658
+ const worker = async () => {
3659
+ while (nextIdx < componentNames.length) {
3660
+ const i = nextIdx++;
3661
+ const name = componentNames[i];
3662
+ if (name !== void 0) {
3663
+ await renderOne(name);
3664
+ }
3665
+ }
3666
+ };
3667
+ const workers = [];
3668
+ for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
3669
+ workers.push(worker());
3670
+ }
3671
+ await Promise.all(workers);
3672
+ }
3673
+ await shutdownPool5();
3674
+ if (isTTY() && total > 0) {
3675
+ process.stderr.write("\n");
3676
+ }
3677
+ const resolver = new TokenResolver2([]);
3678
+ const engine = new ComplianceEngine2(resolver);
3679
+ const currentBatchReport = engine.auditBatch(computedStylesMap);
3680
+ const entries = [];
3681
+ for (const name of componentNames) {
3682
+ const baselineComp = baselineCompliance?.components[name] ?? null;
3683
+ const currentComp = currentBatchReport.components[name] ?? null;
3684
+ const baselineMeta = loadBaselineRenderJson(baselineDir, name);
3685
+ const currentMeta = currentRenderMeta.get(name) ?? null;
3686
+ const failed = renderFailures.has(name);
3687
+ const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
3688
+ const currentComplianceScore = currentComp?.compliance ?? null;
3689
+ const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
3690
+ const partial = {
3691
+ name,
3692
+ baselineCompliance: baselineComplianceScore,
3693
+ currentCompliance: currentComplianceScore,
3694
+ complianceDelta: delta,
3695
+ baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
3696
+ currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
3697
+ renderTimeMs: currentMeta?.renderTimeMs ?? null,
3698
+ renderFailed: failed
3699
+ };
3700
+ entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
3701
+ }
3702
+ for (const name of removedNames) {
3703
+ const baselineComp = baselineCompliance?.components[name] ?? null;
3704
+ const baselineMeta = loadBaselineRenderJson(baselineDir, name);
3705
+ entries.push({
3706
+ name,
3707
+ status: "removed",
3708
+ baselineCompliance: baselineComp?.aggregateCompliance ?? null,
3709
+ currentCompliance: null,
3710
+ complianceDelta: null,
3711
+ baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
3712
+ currentDimensions: null,
3713
+ renderTimeMs: null,
3714
+ renderFailed: false
3715
+ });
3716
+ }
3717
+ const summary = {
3718
+ total: entries.length,
3719
+ added: entries.filter((e) => e.status === "added").length,
3720
+ removed: entries.filter((e) => e.status === "removed").length,
3721
+ unchanged: entries.filter((e) => e.status === "unchanged").length,
3722
+ complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
3723
+ complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
3724
+ sizeChanged: entries.filter((e) => e.status === "size_changed").length,
3725
+ renderFailed: entries.filter((e) => e.renderFailed).length
3726
+ };
3727
+ const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
3728
+ const wallClockMs = performance.now() - startTime;
3729
+ return {
3730
+ diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
3731
+ baselineDir,
3732
+ summary,
3733
+ components: entries,
3734
+ baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
3735
+ currentAggregateCompliance: currentBatchReport.aggregateCompliance,
3736
+ hasRegressions,
3737
+ wallClockMs
3738
+ };
3739
+ }
3740
+ var STATUS_ICON = {
3741
+ added: "+",
3742
+ removed: "-",
3743
+ unchanged: " ",
3744
+ compliance_regressed: "\u2193",
3745
+ compliance_improved: "\u2191",
3746
+ size_changed: "~"
3747
+ };
3748
+ var STATUS_LABEL = {
3749
+ added: "added",
3750
+ removed: "removed",
3751
+ unchanged: "ok",
3752
+ compliance_regressed: "regressed",
3753
+ compliance_improved: "improved",
3754
+ size_changed: "size changed"
3755
+ };
3756
+ function formatDiffReport(result) {
3757
+ const lines = [];
3758
+ const title = "Scope Report Diff";
3759
+ const rule2 = "\u2501".repeat(Math.max(title.length, 40));
3760
+ lines.push(title, rule2);
3761
+ const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
3762
+ const complianceSign = complianceDelta >= 0 ? "+" : "";
3763
+ lines.push(
3764
+ `Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
3765
+ `Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
3766
+ `Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
3767
+ ""
3768
+ );
3769
+ const s = result.summary;
3770
+ lines.push(
3771
+ `Components: ${s.total} total ` + [
3772
+ s.added > 0 ? `${s.added} added` : "",
3773
+ s.removed > 0 ? `${s.removed} removed` : "",
3774
+ s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
3775
+ s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
3776
+ s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
3777
+ s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
3778
+ ].filter(Boolean).join(" "),
3779
+ ""
3780
+ );
3781
+ const notable = result.components.filter((e) => e.status !== "unchanged");
3782
+ if (notable.length === 0) {
3783
+ lines.push(" No changes detected.");
3784
+ } else {
3785
+ const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
3786
+ const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
3787
+ const divider = "-".repeat(header.length);
3788
+ lines.push(header, divider);
3789
+ for (const entry of notable) {
3790
+ const icon = STATUS_ICON[entry.status];
3791
+ const label = STATUS_LABEL[entry.status].padEnd(13);
3792
+ const name = entry.name.padEnd(nameWidth);
3793
+ let complianceStr = "\u2014".padEnd(13);
3794
+ if (entry.complianceDelta !== null) {
3795
+ const sign = entry.complianceDelta >= 0 ? "+" : "";
3796
+ complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
3797
+ }
3798
+ let dimStr = "\u2014";
3799
+ if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
3800
+ const b = entry.baselineDimensions;
3801
+ const c = entry.currentDimensions;
3802
+ if (b.width !== c.width || b.height !== c.height) {
3803
+ dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
3804
+ } else {
3805
+ dimStr = `${c.width}\xD7${c.height}`;
3806
+ }
3807
+ } else if (entry.currentDimensions !== null) {
3808
+ dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
3809
+ } else if (entry.baselineDimensions !== null) {
3810
+ dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
3811
+ }
3812
+ if (entry.renderFailed) {
3813
+ dimStr = "render failed";
3814
+ }
3815
+ lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
3816
+ }
3817
+ }
3818
+ lines.push(
3819
+ "",
3820
+ rule2,
3821
+ 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`
3822
+ );
3823
+ return lines.join("\n");
3824
+ }
3825
+ function registerDiffSubCommand(reportCmd) {
3826
+ 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(
3827
+ "--regression-threshold <n>",
3828
+ "Minimum compliance drop (0\u20131) to classify as a regression",
3829
+ "0.01"
3830
+ ).action(
3831
+ async (opts) => {
3832
+ try {
3833
+ const [wStr, hStr] = opts.viewport.split("x");
3834
+ const viewportWidth = Number.parseInt(wStr ?? "375", 10);
3835
+ const viewportHeight = Number.parseInt(hStr ?? "812", 10);
3836
+ const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
3837
+ const result = await runDiff({
3838
+ baselineDir: opts.baseline,
3839
+ componentsGlob: opts.components,
3840
+ manifestPath: opts.manifest,
3841
+ viewportWidth,
3842
+ viewportHeight,
3843
+ regressionThreshold
3844
+ });
3845
+ if (opts.output !== void 0) {
3846
+ writeFileSync6(opts.output, JSON.stringify(result, null, 2), "utf-8");
3847
+ process.stderr.write(`Diff written to ${opts.output}
3848
+ `);
3849
+ }
3850
+ if (opts.json) {
3851
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
3852
+ `);
3853
+ } else {
3854
+ process.stdout.write(`${formatDiffReport(result)}
3855
+ `);
3856
+ }
3857
+ process.exit(result.hasRegressions ? 1 : 0);
3858
+ } catch (err) {
3859
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3860
+ `);
3861
+ process.exit(2);
3862
+ }
3863
+ }
3864
+ );
3865
+ }
3866
+
3387
3867
  // src/tree-formatter.ts
3388
3868
  var BRANCH2 = "\u251C\u2500\u2500 ";
3389
3869
  var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
@@ -3664,25 +4144,25 @@ function buildStructuredReport(report) {
3664
4144
  }
3665
4145
 
3666
4146
  // src/tokens/commands.ts
3667
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
3668
- import { resolve as resolve10 } from "path";
4147
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
4148
+ import { resolve as resolve11 } from "path";
3669
4149
  import {
3670
4150
  parseTokenFileSync as parseTokenFileSync2,
3671
4151
  TokenParseError,
3672
- TokenResolver as TokenResolver3,
4152
+ TokenResolver as TokenResolver4,
3673
4153
  TokenValidationError,
3674
4154
  validateTokenFile
3675
4155
  } from "@agent-scope/tokens";
3676
4156
  import { Command as Command7 } from "commander";
3677
4157
 
3678
4158
  // src/tokens/export.ts
3679
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3680
- import { resolve as resolve9 } from "path";
4159
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
4160
+ import { resolve as resolve10 } from "path";
3681
4161
  import {
3682
4162
  exportTokens,
3683
4163
  parseTokenFileSync,
3684
4164
  ThemeResolver,
3685
- TokenResolver as TokenResolver2
4165
+ TokenResolver as TokenResolver3
3686
4166
  } from "@agent-scope/tokens";
3687
4167
  import { Command as Command6 } from "commander";
3688
4168
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
@@ -3690,21 +4170,21 @@ var CONFIG_FILE = "reactscope.config.json";
3690
4170
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
3691
4171
  function resolveTokenFilePath(fileFlag) {
3692
4172
  if (fileFlag !== void 0) {
3693
- return resolve9(process.cwd(), fileFlag);
4173
+ return resolve10(process.cwd(), fileFlag);
3694
4174
  }
3695
- const configPath = resolve9(process.cwd(), CONFIG_FILE);
3696
- if (existsSync6(configPath)) {
4175
+ const configPath = resolve10(process.cwd(), CONFIG_FILE);
4176
+ if (existsSync7(configPath)) {
3697
4177
  try {
3698
- const raw = readFileSync5(configPath, "utf-8");
4178
+ const raw = readFileSync6(configPath, "utf-8");
3699
4179
  const config = JSON.parse(raw);
3700
4180
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3701
4181
  const file = config.tokens.file;
3702
- return resolve9(process.cwd(), file);
4182
+ return resolve10(process.cwd(), file);
3703
4183
  }
3704
4184
  } catch {
3705
4185
  }
3706
4186
  }
3707
- return resolve9(process.cwd(), DEFAULT_TOKEN_FILE);
4187
+ return resolve10(process.cwd(), DEFAULT_TOKEN_FILE);
3708
4188
  }
3709
4189
  function createTokensExportCommand() {
3710
4190
  return new Command6("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
@@ -3723,13 +4203,13 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
3723
4203
  const format = opts.format;
3724
4204
  try {
3725
4205
  const filePath = resolveTokenFilePath(opts.file);
3726
- if (!existsSync6(filePath)) {
4206
+ if (!existsSync7(filePath)) {
3727
4207
  throw new Error(
3728
4208
  `Token file not found at ${filePath}.
3729
4209
  Create a reactscope.tokens.json file or use --file to specify a path.`
3730
4210
  );
3731
4211
  }
3732
- const raw = readFileSync5(filePath, "utf-8");
4212
+ const raw = readFileSync6(filePath, "utf-8");
3733
4213
  const { tokens, rawFile } = parseTokenFileSync(raw);
3734
4214
  let themesMap;
3735
4215
  if (opts.theme !== void 0) {
@@ -3740,7 +4220,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
3740
4220
  Available themes: ${available}`
3741
4221
  );
3742
4222
  }
3743
- const baseResolver = new TokenResolver2(tokens);
4223
+ const baseResolver = new TokenResolver3(tokens);
3744
4224
  const themeResolver = ThemeResolver.fromTokenFile(
3745
4225
  baseResolver,
3746
4226
  rawFile
@@ -3768,8 +4248,8 @@ Available themes: ${themeNames.join(", ")}`
3768
4248
  themes: themesMap
3769
4249
  });
3770
4250
  if (opts.out !== void 0) {
3771
- const outPath = resolve9(process.cwd(), opts.out);
3772
- writeFileSync6(outPath, output, "utf-8");
4251
+ const outPath = resolve10(process.cwd(), opts.out);
4252
+ writeFileSync7(outPath, output, "utf-8");
3773
4253
  process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
3774
4254
  `);
3775
4255
  } else {
@@ -3809,30 +4289,30 @@ function buildTable2(headers, rows) {
3809
4289
  }
3810
4290
  function resolveTokenFilePath2(fileFlag) {
3811
4291
  if (fileFlag !== void 0) {
3812
- return resolve10(process.cwd(), fileFlag);
4292
+ return resolve11(process.cwd(), fileFlag);
3813
4293
  }
3814
- const configPath = resolve10(process.cwd(), CONFIG_FILE2);
3815
- if (existsSync7(configPath)) {
4294
+ const configPath = resolve11(process.cwd(), CONFIG_FILE2);
4295
+ if (existsSync8(configPath)) {
3816
4296
  try {
3817
- const raw = readFileSync6(configPath, "utf-8");
4297
+ const raw = readFileSync7(configPath, "utf-8");
3818
4298
  const config = JSON.parse(raw);
3819
4299
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3820
4300
  const file = config.tokens.file;
3821
- return resolve10(process.cwd(), file);
4301
+ return resolve11(process.cwd(), file);
3822
4302
  }
3823
4303
  } catch {
3824
4304
  }
3825
4305
  }
3826
- return resolve10(process.cwd(), DEFAULT_TOKEN_FILE2);
4306
+ return resolve11(process.cwd(), DEFAULT_TOKEN_FILE2);
3827
4307
  }
3828
4308
  function loadTokens(absPath) {
3829
- if (!existsSync7(absPath)) {
4309
+ if (!existsSync8(absPath)) {
3830
4310
  throw new Error(
3831
4311
  `Token file not found at ${absPath}.
3832
4312
  Create a reactscope.tokens.json file or use --file to specify a path.`
3833
4313
  );
3834
4314
  }
3835
- const raw = readFileSync6(absPath, "utf-8");
4315
+ const raw = readFileSync7(absPath, "utf-8");
3836
4316
  return parseTokenFileSync2(raw);
3837
4317
  }
3838
4318
  function getRawValue(node, segments) {
@@ -3872,7 +4352,7 @@ function registerGet2(tokensCmd) {
3872
4352
  try {
3873
4353
  const filePath = resolveTokenFilePath2(opts.file);
3874
4354
  const { tokens } = loadTokens(filePath);
3875
- const resolver = new TokenResolver3(tokens);
4355
+ const resolver = new TokenResolver4(tokens);
3876
4356
  const resolvedValue = resolver.resolve(tokenPath);
3877
4357
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
3878
4358
  if (useJson) {
@@ -3898,7 +4378,7 @@ function registerList2(tokensCmd) {
3898
4378
  try {
3899
4379
  const filePath = resolveTokenFilePath2(opts.file);
3900
4380
  const { tokens } = loadTokens(filePath);
3901
- const resolver = new TokenResolver3(tokens);
4381
+ const resolver = new TokenResolver4(tokens);
3902
4382
  const filtered = resolver.list(opts.type, category);
3903
4383
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
3904
4384
  if (useJson) {
@@ -3928,7 +4408,7 @@ function registerSearch(tokensCmd) {
3928
4408
  try {
3929
4409
  const filePath = resolveTokenFilePath2(opts.file);
3930
4410
  const { tokens } = loadTokens(filePath);
3931
- const resolver = new TokenResolver3(tokens);
4411
+ const resolver = new TokenResolver4(tokens);
3932
4412
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
3933
4413
  const typesToSearch = opts.type ? [opts.type] : [
3934
4414
  "color",
@@ -4011,7 +4491,7 @@ function registerResolve(tokensCmd) {
4011
4491
  const filePath = resolveTokenFilePath2(opts.file);
4012
4492
  const absFilePath = filePath;
4013
4493
  const { tokens, rawFile } = loadTokens(absFilePath);
4014
- const resolver = new TokenResolver3(tokens);
4494
+ const resolver = new TokenResolver4(tokens);
4015
4495
  resolver.resolve(tokenPath);
4016
4496
  const chain = buildResolutionChain(tokenPath, rawFile.tokens);
4017
4497
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
@@ -4046,13 +4526,13 @@ function registerValidate(tokensCmd) {
4046
4526
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
4047
4527
  try {
4048
4528
  const filePath = resolveTokenFilePath2(opts.file);
4049
- if (!existsSync7(filePath)) {
4529
+ if (!existsSync8(filePath)) {
4050
4530
  throw new Error(
4051
4531
  `Token file not found at ${filePath}.
4052
4532
  Create a reactscope.tokens.json file or use --file to specify a path.`
4053
4533
  );
4054
4534
  }
4055
- const raw = readFileSync6(filePath, "utf-8");
4535
+ const raw = readFileSync7(filePath, "utf-8");
4056
4536
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
4057
4537
  const errors = [];
4058
4538
  let parsed;
@@ -4207,7 +4687,7 @@ function createProgram(options = {}) {
4207
4687
  }
4208
4688
  );
4209
4689
  program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
4210
- const raw = readFileSync7(tracePath, "utf-8");
4690
+ const raw = readFileSync8(tracePath, "utf-8");
4211
4691
  const trace = loadTrace(raw);
4212
4692
  const source = generateTest(trace, {
4213
4693
  description: opts.description,
@@ -4224,6 +4704,7 @@ function createProgram(options = {}) {
4224
4704
  const existingReportCmd = program2.commands.find((c) => c.name() === "report");
4225
4705
  if (existingReportCmd !== void 0) {
4226
4706
  registerBaselineSubCommand(existingReportCmd);
4707
+ registerDiffSubCommand(existingReportCmd);
4227
4708
  }
4228
4709
  return program2;
4229
4710
  }