@agent-scope/cli 1.11.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,9 +1,9 @@
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
- import { Command as Command7 } from "commander";
6
+ import { Command as Command8 } from "commander";
7
7
 
8
8
  // src/browser.ts
9
9
  import { writeFileSync } from "fs";
@@ -255,9 +255,9 @@ function createRL() {
255
255
  });
256
256
  }
257
257
  async function ask(rl, question) {
258
- return new Promise((resolve10) => {
258
+ return new Promise((resolve12) => {
259
259
  rl.question(question, (answer) => {
260
- resolve10(answer.trim());
260
+ resolve12(answer.trim());
261
261
  });
262
262
  });
263
263
  }
@@ -406,9 +406,9 @@ function createInitCommand() {
406
406
  }
407
407
 
408
408
  // src/instrument/renders.ts
409
- import { resolve as resolve4 } from "path";
410
- import { BrowserPool } from "@agent-scope/render";
411
- import { Command as Command3 } from "commander";
409
+ import { resolve as resolve5 } from "path";
410
+ import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
411
+ import { Command as Command4 } from "commander";
412
412
 
413
413
  // src/component-bundler.ts
414
414
  import { dirname } from "path";
@@ -1602,8 +1602,371 @@ Available: ${available}`
1602
1602
  return cmd;
1603
1603
  }
1604
1604
 
1605
- // src/instrument/renders.ts
1605
+ // src/instrument/tree.ts
1606
+ import { resolve as resolve4 } from "path";
1607
+ import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
1608
+ import { BrowserPool } from "@agent-scope/render";
1609
+ import { Command as Command3 } from "commander";
1606
1610
  var MANIFEST_PATH4 = ".reactscope/manifest.json";
1611
+ var DEFAULT_VIEWPORT_WIDTH = 375;
1612
+ var DEFAULT_VIEWPORT_HEIGHT = 812;
1613
+ var _pool = null;
1614
+ async function getPool() {
1615
+ if (_pool === null) {
1616
+ _pool = new BrowserPool({
1617
+ size: { browsers: 1, pagesPerBrowser: 1 },
1618
+ viewportWidth: DEFAULT_VIEWPORT_WIDTH,
1619
+ viewportHeight: DEFAULT_VIEWPORT_HEIGHT
1620
+ });
1621
+ await _pool.init();
1622
+ }
1623
+ return _pool;
1624
+ }
1625
+ async function shutdownPool() {
1626
+ if (_pool !== null) {
1627
+ await _pool.close();
1628
+ _pool = null;
1629
+ }
1630
+ }
1631
+ function mapNodeType(node) {
1632
+ if (node.type === "forward_ref") return "forwardRef";
1633
+ if (node.type === "host") return "host";
1634
+ const name = node.name;
1635
+ if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
1636
+ if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
1637
+ return node.type;
1638
+ }
1639
+ function flattenSerializedValue(sv) {
1640
+ if (sv === null || sv === void 0) return null;
1641
+ const v = sv;
1642
+ switch (v.type) {
1643
+ case "null":
1644
+ case "undefined":
1645
+ return null;
1646
+ case "string":
1647
+ case "number":
1648
+ case "boolean":
1649
+ return v.value;
1650
+ case "object": {
1651
+ if (!Array.isArray(v.entries)) return {};
1652
+ const result = {};
1653
+ for (const entry of v.entries) {
1654
+ result[entry.key] = flattenSerializedValue(entry.value);
1655
+ }
1656
+ return result;
1657
+ }
1658
+ case "array": {
1659
+ if (!Array.isArray(v.items)) return [];
1660
+ return v.items.map(flattenSerializedValue);
1661
+ }
1662
+ case "function":
1663
+ return "[Function]";
1664
+ case "symbol":
1665
+ return `[Symbol: ${v.description ?? ""}]`;
1666
+ case "circular":
1667
+ return "[Circular]";
1668
+ case "truncated":
1669
+ return `[Truncated: ${v.preview ?? ""}]`;
1670
+ default:
1671
+ return v.preview ?? null;
1672
+ }
1673
+ }
1674
+ function flattenHookState(hooks) {
1675
+ const result = {};
1676
+ for (let i = 0; i < hooks.length; i++) {
1677
+ const hook = hooks[i];
1678
+ if (hook === void 0) continue;
1679
+ const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
1680
+ result[key] = flattenSerializedValue(hook.value);
1681
+ }
1682
+ return result;
1683
+ }
1684
+ function extractContextNames(contexts) {
1685
+ const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
1686
+ return names;
1687
+ }
1688
+ function anyContextChanged(contexts) {
1689
+ return contexts.some((c) => c.didTriggerRender);
1690
+ }
1691
+ function convertToInstrumentNode(node, depth = 0) {
1692
+ const contexts = extractContextNames(node.context);
1693
+ const contextChanged = anyContextChanged(node.context);
1694
+ const state = flattenHookState(node.state);
1695
+ const propsFlat = flattenSerializedValue(node.props);
1696
+ const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
1697
+ return {
1698
+ component: node.name,
1699
+ type: mapNodeType(node),
1700
+ renderCount: node.renderCount,
1701
+ lastRenderDuration: node.renderDuration,
1702
+ memoized: node.type === "memo",
1703
+ // memoSkipped requires tracking bail-outs across commits — not available from
1704
+ // a single-shot capture. Defaulted to 0.
1705
+ memoSkipped: 0,
1706
+ props,
1707
+ // propsChanged is not tracked in a single-shot capture — would need a diff
1708
+ // between two renders. Defaulted to false.
1709
+ propsChanged: false,
1710
+ state,
1711
+ stateChanged: false,
1712
+ contextChanged,
1713
+ contexts,
1714
+ depth,
1715
+ children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
1716
+ };
1717
+ }
1718
+ function filterByContext(node, contextName) {
1719
+ const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
1720
+ const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
1721
+ if (!selfMatches && filteredChildren.length === 0) return null;
1722
+ return { ...node, children: filteredChildren };
1723
+ }
1724
+ function filterWastedRenders(node) {
1725
+ const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
1726
+ const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
1727
+ if (!isWasted && filteredChildren.length === 0) return null;
1728
+ return { ...node, children: filteredChildren };
1729
+ }
1730
+ function sortTree(node, sortBy) {
1731
+ const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
1732
+ if (sortBy === "renderCount") return b.renderCount - a.renderCount;
1733
+ return a.depth - b.depth;
1734
+ });
1735
+ return { ...node, children: sortedChildren };
1736
+ }
1737
+ function annotateProviderDepth(node, providerDepth = 0) {
1738
+ const isProvider = node.type === "context.provider";
1739
+ const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
1740
+ return {
1741
+ ...node,
1742
+ _providerDepth: providerDepth,
1743
+ children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
1744
+ };
1745
+ }
1746
+ function limitNodes(root, limit) {
1747
+ let remaining = limit;
1748
+ const clip = (node) => {
1749
+ if (remaining <= 0) return null;
1750
+ remaining--;
1751
+ const clippedChildren = [];
1752
+ for (const child of node.children) {
1753
+ const clipped = clip(child);
1754
+ if (clipped !== null) clippedChildren.push(clipped);
1755
+ }
1756
+ return { ...node, children: clippedChildren };
1757
+ };
1758
+ return clip(root) ?? root;
1759
+ }
1760
+ var BRANCH = "\u251C\u2500\u2500 ";
1761
+ var LAST_BRANCH = "\u2514\u2500\u2500 ";
1762
+ var VERTICAL = "\u2502 ";
1763
+ var EMPTY = " ";
1764
+ function buildTTYLabel(node, showProviderDepth) {
1765
+ const parts = [node.component];
1766
+ switch (node.type) {
1767
+ case "memo":
1768
+ parts.push("[memo]");
1769
+ break;
1770
+ case "forwardRef":
1771
+ parts.push("[forwardRef]");
1772
+ break;
1773
+ case "class":
1774
+ parts.push("[class]");
1775
+ break;
1776
+ case "context.provider":
1777
+ parts.push("[provider]");
1778
+ break;
1779
+ case "context.consumer":
1780
+ parts.push("[consumer]");
1781
+ break;
1782
+ default:
1783
+ break;
1784
+ }
1785
+ if (node.renderCount > 0) {
1786
+ const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
1787
+ parts.push(`(renders:${node.renderCount}${durationStr})`);
1788
+ }
1789
+ if (node.contexts.length > 0) {
1790
+ parts.push(`[ctx:${node.contexts.join(",")}]`);
1791
+ }
1792
+ if (showProviderDepth) {
1793
+ const pd = node._providerDepth;
1794
+ if (pd !== void 0 && pd > 0) {
1795
+ parts.push(`[pd:${pd}]`);
1796
+ }
1797
+ }
1798
+ return parts.join(" ");
1799
+ }
1800
+ function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
1801
+ if (node.type === "host") {
1802
+ for (let i = 0; i < node.children.length; i++) {
1803
+ const child = node.children[i];
1804
+ if (child !== void 0) {
1805
+ renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
1806
+ }
1807
+ }
1808
+ return;
1809
+ }
1810
+ const connector = isLast ? LAST_BRANCH : BRANCH;
1811
+ lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
1812
+ const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
1813
+ for (let i = 0; i < node.children.length; i++) {
1814
+ const child = node.children[i];
1815
+ if (child !== void 0) {
1816
+ renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
1817
+ }
1818
+ }
1819
+ }
1820
+ function formatInstrumentTree(root, showProviderDepth = false) {
1821
+ const lines = [];
1822
+ if (root.type !== "host") {
1823
+ lines.push(buildTTYLabel(root, showProviderDepth));
1824
+ for (let i = 0; i < root.children.length; i++) {
1825
+ const child = root.children[i];
1826
+ if (child !== void 0) {
1827
+ renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
1828
+ }
1829
+ }
1830
+ } else {
1831
+ for (let i = 0; i < root.children.length; i++) {
1832
+ const child = root.children[i];
1833
+ if (child !== void 0) {
1834
+ renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
1835
+ }
1836
+ }
1837
+ }
1838
+ return lines.join("\n");
1839
+ }
1840
+ async function runInstrumentTree(options) {
1841
+ const { componentName, filePath } = options;
1842
+ const pool = await getPool();
1843
+ const slot = await pool.acquire();
1844
+ const { page } = slot;
1845
+ try {
1846
+ await page.addInitScript({ content: getBrowserEntryScript2() });
1847
+ const htmlHarness = await buildComponentHarness(
1848
+ filePath,
1849
+ componentName,
1850
+ {},
1851
+ DEFAULT_VIEWPORT_WIDTH
1852
+ );
1853
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1854
+ await page.waitForFunction(
1855
+ () => {
1856
+ const w = window;
1857
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
1858
+ },
1859
+ { timeout: 15e3 }
1860
+ );
1861
+ const renderError = await page.evaluate(
1862
+ () => window.__SCOPE_RENDER_ERROR__ ?? null
1863
+ );
1864
+ if (renderError !== null) {
1865
+ throw new Error(`Component render error: ${renderError}`);
1866
+ }
1867
+ const captureJson = await page.evaluate(async () => {
1868
+ const w = window;
1869
+ if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
1870
+ throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
1871
+ }
1872
+ return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
1873
+ });
1874
+ const captureResult = JSON.parse(captureJson);
1875
+ const componentTree = captureResult.tree;
1876
+ if (componentTree === void 0 || componentTree === null) {
1877
+ throw new Error(`No component tree found for "${componentName}"`);
1878
+ }
1879
+ let instrumentRoot = convertToInstrumentNode(componentTree, 0);
1880
+ if (options.usesContext !== void 0) {
1881
+ const filtered = filterByContext(instrumentRoot, options.usesContext);
1882
+ instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
1883
+ }
1884
+ if (options.wastedRenders === true) {
1885
+ const filtered = filterWastedRenders(instrumentRoot);
1886
+ instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
1887
+ }
1888
+ if (options.sortBy !== void 0) {
1889
+ instrumentRoot = sortTree(instrumentRoot, options.sortBy);
1890
+ }
1891
+ if (options.providerDepth === true) {
1892
+ instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
1893
+ }
1894
+ if (options.limit !== void 0 && options.limit > 0) {
1895
+ instrumentRoot = limitNodes(instrumentRoot, options.limit);
1896
+ }
1897
+ return instrumentRoot;
1898
+ } finally {
1899
+ pool.release(slot);
1900
+ }
1901
+ }
1902
+ function createInstrumentTreeCommand() {
1903
+ return new Command3("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
1904
+ "--wasted-renders",
1905
+ "Filter to components with wasted renders (no prop/state/context changes, not memoized)",
1906
+ false
1907
+ ).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
1908
+ try {
1909
+ const manifest = loadManifest(opts.manifest);
1910
+ const descriptor = manifest.components[componentName];
1911
+ if (descriptor === void 0) {
1912
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1913
+ throw new Error(
1914
+ `Component "${componentName}" not found in manifest.
1915
+ Available: ${available}`
1916
+ );
1917
+ }
1918
+ if (opts.sortBy !== void 0) {
1919
+ const allowed = ["renderCount", "depth"];
1920
+ if (!allowed.includes(opts.sortBy)) {
1921
+ throw new Error(
1922
+ `Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
1923
+ );
1924
+ }
1925
+ }
1926
+ const rootDir = process.cwd();
1927
+ const filePath = resolve4(rootDir, descriptor.filePath);
1928
+ process.stderr.write(`Instrumenting ${componentName}\u2026
1929
+ `);
1930
+ const instrumentRoot = await runInstrumentTree({
1931
+ componentName,
1932
+ filePath,
1933
+ sortBy: opts.sortBy,
1934
+ limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
1935
+ usesContext: opts.usesContext,
1936
+ providerDepth: opts.providerDepth,
1937
+ wastedRenders: opts.wastedRenders
1938
+ });
1939
+ await shutdownPool();
1940
+ const fmt = resolveFormat2(opts.format);
1941
+ if (fmt === "json") {
1942
+ process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
1943
+ `);
1944
+ } else {
1945
+ const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
1946
+ process.stdout.write(`${tree}
1947
+ `);
1948
+ }
1949
+ } catch (err) {
1950
+ await shutdownPool();
1951
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1952
+ `);
1953
+ process.exit(1);
1954
+ }
1955
+ });
1956
+ }
1957
+ function resolveFormat2(formatFlag) {
1958
+ if (formatFlag !== void 0) {
1959
+ const lower = formatFlag.toLowerCase();
1960
+ if (lower !== "json" && lower !== "tree") {
1961
+ throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
1962
+ }
1963
+ return lower;
1964
+ }
1965
+ return isTTY() ? "tree" : "json";
1966
+ }
1967
+
1968
+ // src/instrument/renders.ts
1969
+ var MANIFEST_PATH5 = ".reactscope/manifest.json";
1607
1970
  function determineTrigger(event) {
1608
1971
  if (event.forceUpdate) return "force_update";
1609
1972
  if (event.stateChanged) return "state_change";
@@ -1904,26 +2267,26 @@ async function replayInteraction2(page, steps) {
1904
2267
  }
1905
2268
  }
1906
2269
  }
1907
- var _pool = null;
1908
- async function getPool() {
1909
- if (_pool === null) {
1910
- _pool = new BrowserPool({
2270
+ var _pool2 = null;
2271
+ async function getPool2() {
2272
+ if (_pool2 === null) {
2273
+ _pool2 = new BrowserPool2({
1911
2274
  size: { browsers: 1, pagesPerBrowser: 2 },
1912
2275
  viewportWidth: 1280,
1913
2276
  viewportHeight: 800
1914
2277
  });
1915
- await _pool.init();
2278
+ await _pool2.init();
1916
2279
  }
1917
- return _pool;
2280
+ return _pool2;
1918
2281
  }
1919
- async function shutdownPool() {
1920
- if (_pool !== null) {
1921
- await _pool.close();
1922
- _pool = null;
2282
+ async function shutdownPool2() {
2283
+ if (_pool2 !== null) {
2284
+ await _pool2.close();
2285
+ _pool2 = null;
1923
2286
  }
1924
2287
  }
1925
2288
  async function analyzeRenders(options) {
1926
- const manifestPath = options.manifestPath ?? MANIFEST_PATH4;
2289
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
1927
2290
  const manifest = loadManifest(manifestPath);
1928
2291
  const descriptor = manifest.components[options.componentName];
1929
2292
  if (descriptor === void 0) {
@@ -1934,9 +2297,9 @@ Available: ${available}`
1934
2297
  );
1935
2298
  }
1936
2299
  const rootDir = process.cwd();
1937
- const filePath = resolve4(rootDir, descriptor.filePath);
2300
+ const filePath = resolve5(rootDir, descriptor.filePath);
1938
2301
  const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
1939
- const pool = await getPool();
2302
+ const pool = await getPool2();
1940
2303
  const slot = await pool.acquire();
1941
2304
  const { page } = slot;
1942
2305
  const startMs = performance.now();
@@ -2017,11 +2380,11 @@ function formatRendersTable(result) {
2017
2380
  return lines.join("\n");
2018
2381
  }
2019
2382
  function createInstrumentRendersCommand() {
2020
- return new Command3("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
2383
+ return new Command4("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
2021
2384
  "--interaction <json>",
2022
2385
  `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
2023
2386
  "[]"
2024
- ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(
2387
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
2025
2388
  async (componentName, opts) => {
2026
2389
  let interaction = [];
2027
2390
  try {
@@ -2044,7 +2407,7 @@ function createInstrumentRendersCommand() {
2044
2407
  interaction,
2045
2408
  manifestPath: opts.manifest
2046
2409
  });
2047
- await shutdownPool();
2410
+ await shutdownPool2();
2048
2411
  if (opts.json || !isTTY()) {
2049
2412
  process.stdout.write(`${JSON.stringify(result, null, 2)}
2050
2413
  `);
@@ -2053,7 +2416,7 @@ function createInstrumentRendersCommand() {
2053
2416
  `);
2054
2417
  }
2055
2418
  } catch (err) {
2056
- await shutdownPool();
2419
+ await shutdownPool2();
2057
2420
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2058
2421
  `);
2059
2422
  process.exit(1);
@@ -2062,34 +2425,35 @@ function createInstrumentRendersCommand() {
2062
2425
  );
2063
2426
  }
2064
2427
  function createInstrumentCommand() {
2065
- const instrumentCmd = new Command3("instrument").description(
2428
+ const instrumentCmd = new Command4("instrument").description(
2066
2429
  "Structured instrumentation commands for React component analysis"
2067
2430
  );
2068
2431
  instrumentCmd.addCommand(createInstrumentRendersCommand());
2069
2432
  instrumentCmd.addCommand(createInstrumentHooksCommand());
2070
2433
  instrumentCmd.addCommand(createInstrumentProfileCommand());
2434
+ instrumentCmd.addCommand(createInstrumentTreeCommand());
2071
2435
  return instrumentCmd;
2072
2436
  }
2073
2437
 
2074
2438
  // src/render-commands.ts
2075
2439
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
2076
- import { resolve as resolve6 } from "path";
2440
+ import { resolve as resolve7 } from "path";
2077
2441
  import {
2078
2442
  ALL_CONTEXT_IDS,
2079
2443
  ALL_STRESS_IDS,
2080
- BrowserPool as BrowserPool2,
2444
+ BrowserPool as BrowserPool3,
2081
2445
  contextAxis,
2082
2446
  RenderMatrix,
2083
2447
  SatoriRenderer,
2084
2448
  safeRender,
2085
2449
  stressAxis
2086
2450
  } from "@agent-scope/render";
2087
- import { Command as Command4 } from "commander";
2451
+ import { Command as Command5 } from "commander";
2088
2452
 
2089
2453
  // src/tailwind-css.ts
2090
2454
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
2091
2455
  import { createRequire } from "module";
2092
- import { resolve as resolve5 } from "path";
2456
+ import { resolve as resolve6 } from "path";
2093
2457
  var CONFIG_FILENAMES = [
2094
2458
  ".reactscope/config.json",
2095
2459
  ".reactscope/config.js",
@@ -2106,14 +2470,14 @@ var STYLE_ENTRY_CANDIDATES = [
2106
2470
  var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
2107
2471
  var compilerCache = null;
2108
2472
  function getCachedBuild(cwd) {
2109
- if (compilerCache !== null && resolve5(compilerCache.cwd) === resolve5(cwd)) {
2473
+ if (compilerCache !== null && resolve6(compilerCache.cwd) === resolve6(cwd)) {
2110
2474
  return compilerCache.build;
2111
2475
  }
2112
2476
  return null;
2113
2477
  }
2114
2478
  function findStylesEntry(cwd) {
2115
2479
  for (const name of CONFIG_FILENAMES) {
2116
- const p = resolve5(cwd, name);
2480
+ const p = resolve6(cwd, name);
2117
2481
  if (!existsSync4(p)) continue;
2118
2482
  try {
2119
2483
  if (name.endsWith(".json")) {
@@ -2122,28 +2486,28 @@ function findStylesEntry(cwd) {
2122
2486
  const scope = data.scope;
2123
2487
  const entry = scope?.stylesEntry ?? data.stylesEntry;
2124
2488
  if (typeof entry === "string") {
2125
- const full = resolve5(cwd, entry);
2489
+ const full = resolve6(cwd, entry);
2126
2490
  if (existsSync4(full)) return full;
2127
2491
  }
2128
2492
  }
2129
2493
  } catch {
2130
2494
  }
2131
2495
  }
2132
- const pkgPath = resolve5(cwd, "package.json");
2496
+ const pkgPath = resolve6(cwd, "package.json");
2133
2497
  if (existsSync4(pkgPath)) {
2134
2498
  try {
2135
2499
  const raw = readFileSync4(pkgPath, "utf-8");
2136
2500
  const pkg = JSON.parse(raw);
2137
2501
  const entry = pkg.scope?.stylesEntry;
2138
2502
  if (typeof entry === "string") {
2139
- const full = resolve5(cwd, entry);
2503
+ const full = resolve6(cwd, entry);
2140
2504
  if (existsSync4(full)) return full;
2141
2505
  }
2142
2506
  } catch {
2143
2507
  }
2144
2508
  }
2145
2509
  for (const candidate of STYLE_ENTRY_CANDIDATES) {
2146
- const full = resolve5(cwd, candidate);
2510
+ const full = resolve6(cwd, candidate);
2147
2511
  if (existsSync4(full)) {
2148
2512
  try {
2149
2513
  const content = readFileSync4(full, "utf-8");
@@ -2161,7 +2525,7 @@ async function getTailwindCompiler(cwd) {
2161
2525
  if (entryPath === null) return null;
2162
2526
  let compile;
2163
2527
  try {
2164
- const require2 = createRequire(resolve5(cwd, "package.json"));
2528
+ const require2 = createRequire(resolve6(cwd, "package.json"));
2165
2529
  const tailwind = require2("tailwindcss");
2166
2530
  const fn = tailwind.compile;
2167
2531
  if (typeof fn !== "function") return null;
@@ -2172,8 +2536,8 @@ async function getTailwindCompiler(cwd) {
2172
2536
  const entryContent = readFileSync4(entryPath, "utf-8");
2173
2537
  const loadStylesheet = async (id, base) => {
2174
2538
  if (id === "tailwindcss") {
2175
- const nodeModules = resolve5(cwd, "node_modules");
2176
- const tailwindCssPath = resolve5(nodeModules, "tailwindcss", "index.css");
2539
+ const nodeModules = resolve6(cwd, "node_modules");
2540
+ const tailwindCssPath = resolve6(nodeModules, "tailwindcss", "index.css");
2177
2541
  if (!existsSync4(tailwindCssPath)) {
2178
2542
  throw new Error(
2179
2543
  `Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
@@ -2182,10 +2546,10 @@ async function getTailwindCompiler(cwd) {
2182
2546
  const content = readFileSync4(tailwindCssPath, "utf-8");
2183
2547
  return { path: "virtual:tailwindcss/index.css", base, content };
2184
2548
  }
2185
- const full = resolve5(base, id);
2549
+ const full = resolve6(base, id);
2186
2550
  if (existsSync4(full)) {
2187
2551
  const content = readFileSync4(full, "utf-8");
2188
- return { path: full, base: resolve5(full, ".."), content };
2552
+ return { path: full, base: resolve6(full, ".."), content };
2189
2553
  }
2190
2554
  throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
2191
2555
  };
@@ -2207,24 +2571,24 @@ async function getCompiledCssForClasses(cwd, classes) {
2207
2571
  }
2208
2572
 
2209
2573
  // src/render-commands.ts
2210
- var MANIFEST_PATH5 = ".reactscope/manifest.json";
2574
+ var MANIFEST_PATH6 = ".reactscope/manifest.json";
2211
2575
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
2212
- var _pool2 = null;
2213
- async function getPool2(viewportWidth, viewportHeight) {
2214
- if (_pool2 === null) {
2215
- _pool2 = new BrowserPool2({
2576
+ var _pool3 = null;
2577
+ async function getPool3(viewportWidth, viewportHeight) {
2578
+ if (_pool3 === null) {
2579
+ _pool3 = new BrowserPool3({
2216
2580
  size: { browsers: 1, pagesPerBrowser: 4 },
2217
2581
  viewportWidth,
2218
2582
  viewportHeight
2219
2583
  });
2220
- await _pool2.init();
2584
+ await _pool3.init();
2221
2585
  }
2222
- return _pool2;
2586
+ return _pool3;
2223
2587
  }
2224
- async function shutdownPool2() {
2225
- if (_pool2 !== null) {
2226
- await _pool2.close();
2227
- _pool2 = null;
2588
+ async function shutdownPool3() {
2589
+ if (_pool3 !== null) {
2590
+ await _pool3.close();
2591
+ _pool3 = null;
2228
2592
  }
2229
2593
  }
2230
2594
  function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
@@ -2235,7 +2599,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
2235
2599
  _satori: satori,
2236
2600
  async renderCell(props, _complexityClass) {
2237
2601
  const startMs = performance.now();
2238
- const pool = await getPool2(viewportWidth, viewportHeight);
2602
+ const pool = await getPool3(viewportWidth, viewportHeight);
2239
2603
  const htmlHarness = await buildComponentHarness(
2240
2604
  filePath,
2241
2605
  componentName,
@@ -2332,7 +2696,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
2332
2696
  };
2333
2697
  }
2334
2698
  function registerRenderSingle(renderCmd) {
2335
- renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
2699
+ renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
2336
2700
  async (componentName, opts) => {
2337
2701
  try {
2338
2702
  const manifest = loadManifest(opts.manifest);
@@ -2354,7 +2718,7 @@ Available: ${available}`
2354
2718
  }
2355
2719
  const { width, height } = parseViewport(opts.viewport);
2356
2720
  const rootDir = process.cwd();
2357
- const filePath = resolve6(rootDir, descriptor.filePath);
2721
+ const filePath = resolve7(rootDir, descriptor.filePath);
2358
2722
  const renderer = buildRenderer(filePath, componentName, width, height);
2359
2723
  process.stderr.write(
2360
2724
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
@@ -2371,7 +2735,7 @@ Available: ${available}`
2371
2735
  }
2372
2736
  }
2373
2737
  );
2374
- await shutdownPool2();
2738
+ await shutdownPool3();
2375
2739
  if (outcome.crashed) {
2376
2740
  process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
2377
2741
  `);
@@ -2384,7 +2748,7 @@ Available: ${available}`
2384
2748
  }
2385
2749
  const result = outcome.result;
2386
2750
  if (opts.output !== void 0) {
2387
- const outPath = resolve6(process.cwd(), opts.output);
2751
+ const outPath = resolve7(process.cwd(), opts.output);
2388
2752
  writeFileSync4(outPath, result.screenshot);
2389
2753
  process.stdout.write(
2390
2754
  `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -2398,9 +2762,9 @@ Available: ${available}`
2398
2762
  process.stdout.write(`${JSON.stringify(json, null, 2)}
2399
2763
  `);
2400
2764
  } else if (fmt === "file") {
2401
- const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
2765
+ const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
2402
2766
  mkdirSync3(dir, { recursive: true });
2403
- const outPath = resolve6(dir, `${componentName}.png`);
2767
+ const outPath = resolve7(dir, `${componentName}.png`);
2404
2768
  writeFileSync4(outPath, result.screenshot);
2405
2769
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
2406
2770
  process.stdout.write(
@@ -2408,9 +2772,9 @@ Available: ${available}`
2408
2772
  `
2409
2773
  );
2410
2774
  } else {
2411
- const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
2775
+ const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
2412
2776
  mkdirSync3(dir, { recursive: true });
2413
- const outPath = resolve6(dir, `${componentName}.png`);
2777
+ const outPath = resolve7(dir, `${componentName}.png`);
2414
2778
  writeFileSync4(outPath, result.screenshot);
2415
2779
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
2416
2780
  process.stdout.write(
@@ -2419,7 +2783,7 @@ Available: ${available}`
2419
2783
  );
2420
2784
  }
2421
2785
  } catch (err) {
2422
- await shutdownPool2();
2786
+ await shutdownPool3();
2423
2787
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2424
2788
  `);
2425
2789
  process.exit(1);
@@ -2431,7 +2795,7 @@ function registerRenderMatrix(renderCmd) {
2431
2795
  renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
2432
2796
  "--contexts <ids>",
2433
2797
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
2434
- ).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
2798
+ ).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
2435
2799
  async (componentName, opts) => {
2436
2800
  try {
2437
2801
  const manifest = loadManifest(opts.manifest);
@@ -2446,7 +2810,7 @@ Available: ${available}`
2446
2810
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
2447
2811
  const { width, height } = { width: 375, height: 812 };
2448
2812
  const rootDir = process.cwd();
2449
- const filePath = resolve6(rootDir, descriptor.filePath);
2813
+ const filePath = resolve7(rootDir, descriptor.filePath);
2450
2814
  const renderer = buildRenderer(filePath, componentName, width, height);
2451
2815
  const axes = [];
2452
2816
  if (opts.axes !== void 0) {
@@ -2504,7 +2868,7 @@ Available: ${available}`
2504
2868
  concurrency
2505
2869
  });
2506
2870
  const result = await matrix.render();
2507
- await shutdownPool2();
2871
+ await shutdownPool3();
2508
2872
  process.stderr.write(
2509
2873
  `Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
2510
2874
  `
@@ -2513,7 +2877,7 @@ Available: ${available}`
2513
2877
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
2514
2878
  const gen = new SpriteSheetGenerator();
2515
2879
  const sheet = await gen.generate(result);
2516
- const spritePath = resolve6(process.cwd(), opts.sprite);
2880
+ const spritePath = resolve7(process.cwd(), opts.sprite);
2517
2881
  writeFileSync4(spritePath, sheet.png);
2518
2882
  process.stderr.write(`Sprite sheet saved to ${spritePath}
2519
2883
  `);
@@ -2523,9 +2887,9 @@ Available: ${available}`
2523
2887
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
2524
2888
  const gen = new SpriteSheetGenerator();
2525
2889
  const sheet = await gen.generate(result);
2526
- const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
2890
+ const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
2527
2891
  mkdirSync3(dir, { recursive: true });
2528
- const outPath = resolve6(dir, `${componentName}-matrix.png`);
2892
+ const outPath = resolve7(dir, `${componentName}-matrix.png`);
2529
2893
  writeFileSync4(outPath, sheet.png);
2530
2894
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
2531
2895
  process.stdout.write(
@@ -2549,7 +2913,7 @@ Available: ${available}`
2549
2913
  process.stdout.write(formatMatrixCsv(componentName, result));
2550
2914
  }
2551
2915
  } catch (err) {
2552
- await shutdownPool2();
2916
+ await shutdownPool3();
2553
2917
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2554
2918
  `);
2555
2919
  process.exit(1);
@@ -2558,7 +2922,7 @@ Available: ${available}`
2558
2922
  );
2559
2923
  }
2560
2924
  function registerRenderAll(renderCmd) {
2561
- renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
2925
+ renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
2562
2926
  async (opts) => {
2563
2927
  try {
2564
2928
  const manifest = loadManifest(opts.manifest);
@@ -2569,7 +2933,7 @@ function registerRenderAll(renderCmd) {
2569
2933
  return;
2570
2934
  }
2571
2935
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
2572
- const outputDir = resolve6(process.cwd(), opts.outputDir);
2936
+ const outputDir = resolve7(process.cwd(), opts.outputDir);
2573
2937
  mkdirSync3(outputDir, { recursive: true });
2574
2938
  const rootDir = process.cwd();
2575
2939
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
@@ -2579,7 +2943,7 @@ function registerRenderAll(renderCmd) {
2579
2943
  const renderOne = async (name) => {
2580
2944
  const descriptor = manifest.components[name];
2581
2945
  if (descriptor === void 0) return;
2582
- const filePath = resolve6(rootDir, descriptor.filePath);
2946
+ const filePath = resolve7(rootDir, descriptor.filePath);
2583
2947
  const renderer = buildRenderer(filePath, name, 375, 812);
2584
2948
  const outcome = await safeRender(
2585
2949
  () => renderer.renderCell({}, descriptor.complexityClass),
@@ -2602,7 +2966,7 @@ function registerRenderAll(renderCmd) {
2602
2966
  success: false,
2603
2967
  errorMessage: outcome.error.message
2604
2968
  });
2605
- const errPath = resolve6(outputDir, `${name}.error.json`);
2969
+ const errPath = resolve7(outputDir, `${name}.error.json`);
2606
2970
  writeFileSync4(
2607
2971
  errPath,
2608
2972
  JSON.stringify(
@@ -2620,9 +2984,9 @@ function registerRenderAll(renderCmd) {
2620
2984
  }
2621
2985
  const result = outcome.result;
2622
2986
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
2623
- const pngPath = resolve6(outputDir, `${name}.png`);
2987
+ const pngPath = resolve7(outputDir, `${name}.png`);
2624
2988
  writeFileSync4(pngPath, result.screenshot);
2625
- const jsonPath = resolve6(outputDir, `${name}.json`);
2989
+ const jsonPath = resolve7(outputDir, `${name}.json`);
2626
2990
  writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
2627
2991
  if (isTTY()) {
2628
2992
  process.stdout.write(
@@ -2646,13 +3010,13 @@ function registerRenderAll(renderCmd) {
2646
3010
  workers.push(worker());
2647
3011
  }
2648
3012
  await Promise.all(workers);
2649
- await shutdownPool2();
3013
+ await shutdownPool3();
2650
3014
  process.stderr.write("\n");
2651
3015
  const summary = formatSummaryText(results, outputDir);
2652
3016
  process.stderr.write(`${summary}
2653
3017
  `);
2654
3018
  } catch (err) {
2655
- await shutdownPool2();
3019
+ await shutdownPool3();
2656
3020
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2657
3021
  `);
2658
3022
  process.exit(1);
@@ -2685,7 +3049,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
2685
3049
  return "json";
2686
3050
  }
2687
3051
  function createRenderCommand() {
2688
- const renderCmd = new Command4("render").description(
3052
+ const renderCmd = new Command5("render").description(
2689
3053
  "Render components to PNG or JSON via esbuild + BrowserPool"
2690
3054
  );
2691
3055
  registerRenderSingle(renderCmd);
@@ -2696,31 +3060,31 @@ function createRenderCommand() {
2696
3060
 
2697
3061
  // src/report/baseline.ts
2698
3062
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
2699
- import { resolve as resolve7 } from "path";
3063
+ import { resolve as resolve8 } from "path";
2700
3064
  import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
2701
- import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
3065
+ import { BrowserPool as BrowserPool4, safeRender as safeRender2 } from "@agent-scope/render";
2702
3066
  import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
2703
3067
  var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
2704
- var _pool3 = null;
2705
- async function getPool3(viewportWidth, viewportHeight) {
2706
- if (_pool3 === null) {
2707
- _pool3 = new BrowserPool3({
3068
+ var _pool4 = null;
3069
+ async function getPool4(viewportWidth, viewportHeight) {
3070
+ if (_pool4 === null) {
3071
+ _pool4 = new BrowserPool4({
2708
3072
  size: { browsers: 1, pagesPerBrowser: 4 },
2709
3073
  viewportWidth,
2710
3074
  viewportHeight
2711
3075
  });
2712
- await _pool3.init();
3076
+ await _pool4.init();
2713
3077
  }
2714
- return _pool3;
3078
+ return _pool4;
2715
3079
  }
2716
- async function shutdownPool3() {
2717
- if (_pool3 !== null) {
2718
- await _pool3.close();
2719
- _pool3 = null;
3080
+ async function shutdownPool4() {
3081
+ if (_pool4 !== null) {
3082
+ await _pool4.close();
3083
+ _pool4 = null;
2720
3084
  }
2721
3085
  }
2722
3086
  async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
2723
- const pool = await getPool3(viewportWidth, viewportHeight);
3087
+ const pool = await getPool4(viewportWidth, viewportHeight);
2724
3088
  const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
2725
3089
  const slot = await pool.acquire();
2726
3090
  const { page } = slot;
@@ -2845,20 +3209,20 @@ async function runBaseline(options = {}) {
2845
3209
  } = options;
2846
3210
  const startTime = performance.now();
2847
3211
  const rootDir = process.cwd();
2848
- const baselineDir = resolve7(rootDir, outputDir);
2849
- const rendersDir = resolve7(baselineDir, "renders");
3212
+ const baselineDir = resolve8(rootDir, outputDir);
3213
+ const rendersDir = resolve8(baselineDir, "renders");
2850
3214
  if (existsSync5(baselineDir)) {
2851
3215
  rmSync(baselineDir, { recursive: true, force: true });
2852
3216
  }
2853
3217
  mkdirSync4(rendersDir, { recursive: true });
2854
3218
  let manifest;
2855
3219
  if (manifestPath !== void 0) {
2856
- const { readFileSync: readFileSync8 } = await import("fs");
2857
- const absPath = resolve7(rootDir, manifestPath);
3220
+ const { readFileSync: readFileSync9 } = await import("fs");
3221
+ const absPath = resolve8(rootDir, manifestPath);
2858
3222
  if (!existsSync5(absPath)) {
2859
3223
  throw new Error(`Manifest not found at ${absPath}.`);
2860
3224
  }
2861
- manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
3225
+ manifest = JSON.parse(readFileSync9(absPath, "utf-8"));
2862
3226
  process.stderr.write(`Loaded manifest from ${manifestPath}
2863
3227
  `);
2864
3228
  } else {
@@ -2868,7 +3232,7 @@ async function runBaseline(options = {}) {
2868
3232
  process.stderr.write(`Found ${count} components.
2869
3233
  `);
2870
3234
  }
2871
- writeFileSync5(resolve7(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
3235
+ writeFileSync5(resolve8(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
2872
3236
  let componentNames = Object.keys(manifest.components);
2873
3237
  if (componentsGlob !== void 0) {
2874
3238
  componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
@@ -2889,7 +3253,7 @@ async function runBaseline(options = {}) {
2889
3253
  auditedAt: (/* @__PURE__ */ new Date()).toISOString()
2890
3254
  };
2891
3255
  writeFileSync5(
2892
- resolve7(baselineDir, "compliance.json"),
3256
+ resolve8(baselineDir, "compliance.json"),
2893
3257
  JSON.stringify(emptyReport, null, 2),
2894
3258
  "utf-8"
2895
3259
  );
@@ -2910,7 +3274,7 @@ async function runBaseline(options = {}) {
2910
3274
  const renderOne = async (name) => {
2911
3275
  const descriptor = manifest.components[name];
2912
3276
  if (descriptor === void 0) return;
2913
- const filePath = resolve7(rootDir, descriptor.filePath);
3277
+ const filePath = resolve8(rootDir, descriptor.filePath);
2914
3278
  const outcome = await safeRender2(
2915
3279
  () => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
2916
3280
  {
@@ -2929,7 +3293,7 @@ async function runBaseline(options = {}) {
2929
3293
  }
2930
3294
  if (outcome.crashed) {
2931
3295
  failureCount++;
2932
- const errPath = resolve7(rendersDir, `${name}.error.json`);
3296
+ const errPath = resolve8(rendersDir, `${name}.error.json`);
2933
3297
  writeFileSync5(
2934
3298
  errPath,
2935
3299
  JSON.stringify(
@@ -2947,10 +3311,10 @@ async function runBaseline(options = {}) {
2947
3311
  return;
2948
3312
  }
2949
3313
  const result = outcome.result;
2950
- writeFileSync5(resolve7(rendersDir, `${name}.png`), result.screenshot);
3314
+ writeFileSync5(resolve8(rendersDir, `${name}.png`), result.screenshot);
2951
3315
  const jsonOutput = formatRenderJson(name, {}, result);
2952
3316
  writeFileSync5(
2953
- resolve7(rendersDir, `${name}.json`),
3317
+ resolve8(rendersDir, `${name}.json`),
2954
3318
  JSON.stringify(jsonOutput, null, 2),
2955
3319
  "utf-8"
2956
3320
  );
@@ -2970,7 +3334,7 @@ async function runBaseline(options = {}) {
2970
3334
  workers.push(worker());
2971
3335
  }
2972
3336
  await Promise.all(workers);
2973
- await shutdownPool3();
3337
+ await shutdownPool4();
2974
3338
  if (isTTY()) {
2975
3339
  process.stderr.write("\n");
2976
3340
  }
@@ -2978,7 +3342,7 @@ async function runBaseline(options = {}) {
2978
3342
  const engine = new ComplianceEngine(resolver);
2979
3343
  const batchReport = engine.auditBatch(computedStylesMap);
2980
3344
  writeFileSync5(
2981
- resolve7(baselineDir, "compliance.json"),
3345
+ resolve8(baselineDir, "compliance.json"),
2982
3346
  JSON.stringify(batchReport, null, 2),
2983
3347
  "utf-8"
2984
3348
  );
@@ -3020,11 +3384,491 @@ function registerBaselineSubCommand(reportCmd) {
3020
3384
  );
3021
3385
  }
3022
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
+
3023
3867
  // src/tree-formatter.ts
3024
- var BRANCH = "\u251C\u2500\u2500 ";
3025
- var LAST_BRANCH = "\u2514\u2500\u2500 ";
3026
- var VERTICAL = "\u2502 ";
3027
- var EMPTY = " ";
3868
+ var BRANCH2 = "\u251C\u2500\u2500 ";
3869
+ var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
3870
+ var VERTICAL2 = "\u2502 ";
3871
+ var EMPTY2 = " ";
3028
3872
  function buildLabel(node, options) {
3029
3873
  const parts = [node.name];
3030
3874
  if (node.type === "memo") {
@@ -3072,19 +3916,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
3072
3916
  }
3073
3917
  return;
3074
3918
  }
3075
- const connector = isLast ? LAST_BRANCH : BRANCH;
3919
+ const connector = isLast ? LAST_BRANCH2 : BRANCH2;
3076
3920
  const label = buildLabel(node, options);
3077
3921
  lines.push(`${prefix}${connector}${label}`);
3078
3922
  if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
3079
3923
  const childCount = countVisibleDescendants(node, options);
3080
3924
  if (childCount > 0) {
3081
- const nextPrefix2 = prefix + (isLast ? EMPTY : VERTICAL);
3082
- lines.push(`${nextPrefix2}${LAST_BRANCH}\u2026 (${childCount} more)`);
3925
+ const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
3926
+ lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
3083
3927
  }
3084
3928
  return;
3085
3929
  }
3086
3930
  const visibleChildren = getVisibleChildren(node, options);
3087
- const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
3931
+ const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
3088
3932
  for (let i = 0; i < visibleChildren.length; i++) {
3089
3933
  const child = visibleChildren[i];
3090
3934
  if (child !== void 0) {
@@ -3126,7 +3970,7 @@ function formatTree(root, options = {}) {
3126
3970
  if (options.maxDepth === 0) {
3127
3971
  const childCount = countVisibleDescendants(root, options);
3128
3972
  if (childCount > 0) {
3129
- lines.push(`${LAST_BRANCH}\u2026 (${childCount} more)`);
3973
+ lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
3130
3974
  }
3131
3975
  } else {
3132
3976
  const visibleChildren = getVisibleChildren(root, options);
@@ -3300,50 +4144,50 @@ function buildStructuredReport(report) {
3300
4144
  }
3301
4145
 
3302
4146
  // src/tokens/commands.ts
3303
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
3304
- import { resolve as resolve9 } from "path";
4147
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
4148
+ import { resolve as resolve11 } from "path";
3305
4149
  import {
3306
4150
  parseTokenFileSync as parseTokenFileSync2,
3307
4151
  TokenParseError,
3308
- TokenResolver as TokenResolver3,
4152
+ TokenResolver as TokenResolver4,
3309
4153
  TokenValidationError,
3310
4154
  validateTokenFile
3311
4155
  } from "@agent-scope/tokens";
3312
- import { Command as Command6 } from "commander";
4156
+ import { Command as Command7 } from "commander";
3313
4157
 
3314
4158
  // src/tokens/export.ts
3315
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3316
- import { resolve as resolve8 } from "path";
4159
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
4160
+ import { resolve as resolve10 } from "path";
3317
4161
  import {
3318
4162
  exportTokens,
3319
4163
  parseTokenFileSync,
3320
4164
  ThemeResolver,
3321
- TokenResolver as TokenResolver2
4165
+ TokenResolver as TokenResolver3
3322
4166
  } from "@agent-scope/tokens";
3323
- import { Command as Command5 } from "commander";
4167
+ import { Command as Command6 } from "commander";
3324
4168
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
3325
4169
  var CONFIG_FILE = "reactscope.config.json";
3326
4170
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
3327
4171
  function resolveTokenFilePath(fileFlag) {
3328
4172
  if (fileFlag !== void 0) {
3329
- return resolve8(process.cwd(), fileFlag);
4173
+ return resolve10(process.cwd(), fileFlag);
3330
4174
  }
3331
- const configPath = resolve8(process.cwd(), CONFIG_FILE);
3332
- if (existsSync6(configPath)) {
4175
+ const configPath = resolve10(process.cwd(), CONFIG_FILE);
4176
+ if (existsSync7(configPath)) {
3333
4177
  try {
3334
- const raw = readFileSync5(configPath, "utf-8");
4178
+ const raw = readFileSync6(configPath, "utf-8");
3335
4179
  const config = JSON.parse(raw);
3336
4180
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3337
4181
  const file = config.tokens.file;
3338
- return resolve8(process.cwd(), file);
4182
+ return resolve10(process.cwd(), file);
3339
4183
  }
3340
4184
  } catch {
3341
4185
  }
3342
4186
  }
3343
- return resolve8(process.cwd(), DEFAULT_TOKEN_FILE);
4187
+ return resolve10(process.cwd(), DEFAULT_TOKEN_FILE);
3344
4188
  }
3345
4189
  function createTokensExportCommand() {
3346
- return new Command5("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(
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(
3347
4191
  "--theme <name>",
3348
4192
  "Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
3349
4193
  ).action(
@@ -3359,13 +4203,13 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
3359
4203
  const format = opts.format;
3360
4204
  try {
3361
4205
  const filePath = resolveTokenFilePath(opts.file);
3362
- if (!existsSync6(filePath)) {
4206
+ if (!existsSync7(filePath)) {
3363
4207
  throw new Error(
3364
4208
  `Token file not found at ${filePath}.
3365
4209
  Create a reactscope.tokens.json file or use --file to specify a path.`
3366
4210
  );
3367
4211
  }
3368
- const raw = readFileSync5(filePath, "utf-8");
4212
+ const raw = readFileSync6(filePath, "utf-8");
3369
4213
  const { tokens, rawFile } = parseTokenFileSync(raw);
3370
4214
  let themesMap;
3371
4215
  if (opts.theme !== void 0) {
@@ -3376,7 +4220,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
3376
4220
  Available themes: ${available}`
3377
4221
  );
3378
4222
  }
3379
- const baseResolver = new TokenResolver2(tokens);
4223
+ const baseResolver = new TokenResolver3(tokens);
3380
4224
  const themeResolver = ThemeResolver.fromTokenFile(
3381
4225
  baseResolver,
3382
4226
  rawFile
@@ -3404,8 +4248,8 @@ Available themes: ${themeNames.join(", ")}`
3404
4248
  themes: themesMap
3405
4249
  });
3406
4250
  if (opts.out !== void 0) {
3407
- const outPath = resolve8(process.cwd(), opts.out);
3408
- writeFileSync6(outPath, output, "utf-8");
4251
+ const outPath = resolve10(process.cwd(), opts.out);
4252
+ writeFileSync7(outPath, output, "utf-8");
3409
4253
  process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
3410
4254
  `);
3411
4255
  } else {
@@ -3445,30 +4289,30 @@ function buildTable2(headers, rows) {
3445
4289
  }
3446
4290
  function resolveTokenFilePath2(fileFlag) {
3447
4291
  if (fileFlag !== void 0) {
3448
- return resolve9(process.cwd(), fileFlag);
4292
+ return resolve11(process.cwd(), fileFlag);
3449
4293
  }
3450
- const configPath = resolve9(process.cwd(), CONFIG_FILE2);
3451
- if (existsSync7(configPath)) {
4294
+ const configPath = resolve11(process.cwd(), CONFIG_FILE2);
4295
+ if (existsSync8(configPath)) {
3452
4296
  try {
3453
- const raw = readFileSync6(configPath, "utf-8");
4297
+ const raw = readFileSync7(configPath, "utf-8");
3454
4298
  const config = JSON.parse(raw);
3455
4299
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3456
4300
  const file = config.tokens.file;
3457
- return resolve9(process.cwd(), file);
4301
+ return resolve11(process.cwd(), file);
3458
4302
  }
3459
4303
  } catch {
3460
4304
  }
3461
4305
  }
3462
- return resolve9(process.cwd(), DEFAULT_TOKEN_FILE2);
4306
+ return resolve11(process.cwd(), DEFAULT_TOKEN_FILE2);
3463
4307
  }
3464
4308
  function loadTokens(absPath) {
3465
- if (!existsSync7(absPath)) {
4309
+ if (!existsSync8(absPath)) {
3466
4310
  throw new Error(
3467
4311
  `Token file not found at ${absPath}.
3468
4312
  Create a reactscope.tokens.json file or use --file to specify a path.`
3469
4313
  );
3470
4314
  }
3471
- const raw = readFileSync6(absPath, "utf-8");
4315
+ const raw = readFileSync7(absPath, "utf-8");
3472
4316
  return parseTokenFileSync2(raw);
3473
4317
  }
3474
4318
  function getRawValue(node, segments) {
@@ -3508,7 +4352,7 @@ function registerGet2(tokensCmd) {
3508
4352
  try {
3509
4353
  const filePath = resolveTokenFilePath2(opts.file);
3510
4354
  const { tokens } = loadTokens(filePath);
3511
- const resolver = new TokenResolver3(tokens);
4355
+ const resolver = new TokenResolver4(tokens);
3512
4356
  const resolvedValue = resolver.resolve(tokenPath);
3513
4357
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
3514
4358
  if (useJson) {
@@ -3534,7 +4378,7 @@ function registerList2(tokensCmd) {
3534
4378
  try {
3535
4379
  const filePath = resolveTokenFilePath2(opts.file);
3536
4380
  const { tokens } = loadTokens(filePath);
3537
- const resolver = new TokenResolver3(tokens);
4381
+ const resolver = new TokenResolver4(tokens);
3538
4382
  const filtered = resolver.list(opts.type, category);
3539
4383
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
3540
4384
  if (useJson) {
@@ -3564,7 +4408,7 @@ function registerSearch(tokensCmd) {
3564
4408
  try {
3565
4409
  const filePath = resolveTokenFilePath2(opts.file);
3566
4410
  const { tokens } = loadTokens(filePath);
3567
- const resolver = new TokenResolver3(tokens);
4411
+ const resolver = new TokenResolver4(tokens);
3568
4412
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
3569
4413
  const typesToSearch = opts.type ? [opts.type] : [
3570
4414
  "color",
@@ -3647,7 +4491,7 @@ function registerResolve(tokensCmd) {
3647
4491
  const filePath = resolveTokenFilePath2(opts.file);
3648
4492
  const absFilePath = filePath;
3649
4493
  const { tokens, rawFile } = loadTokens(absFilePath);
3650
- const resolver = new TokenResolver3(tokens);
4494
+ const resolver = new TokenResolver4(tokens);
3651
4495
  resolver.resolve(tokenPath);
3652
4496
  const chain = buildResolutionChain(tokenPath, rawFile.tokens);
3653
4497
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
@@ -3682,13 +4526,13 @@ function registerValidate(tokensCmd) {
3682
4526
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
3683
4527
  try {
3684
4528
  const filePath = resolveTokenFilePath2(opts.file);
3685
- if (!existsSync7(filePath)) {
4529
+ if (!existsSync8(filePath)) {
3686
4530
  throw new Error(
3687
4531
  `Token file not found at ${filePath}.
3688
4532
  Create a reactscope.tokens.json file or use --file to specify a path.`
3689
4533
  );
3690
4534
  }
3691
- const raw = readFileSync6(filePath, "utf-8");
4535
+ const raw = readFileSync7(filePath, "utf-8");
3692
4536
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
3693
4537
  const errors = [];
3694
4538
  let parsed;
@@ -3756,7 +4600,7 @@ function outputValidationResult(filePath, errors, useJson) {
3756
4600
  }
3757
4601
  }
3758
4602
  function createTokensCommand() {
3759
- const tokensCmd = new Command6("tokens").description(
4603
+ const tokensCmd = new Command7("tokens").description(
3760
4604
  "Query and validate design tokens from a reactscope.tokens.json file"
3761
4605
  );
3762
4606
  registerGet2(tokensCmd);
@@ -3770,7 +4614,7 @@ function createTokensCommand() {
3770
4614
 
3771
4615
  // src/program.ts
3772
4616
  function createProgram(options = {}) {
3773
- const program2 = new Command7("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
4617
+ const program2 = new Command8("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
3774
4618
  program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
3775
4619
  async (url, opts) => {
3776
4620
  try {
@@ -3843,7 +4687,7 @@ function createProgram(options = {}) {
3843
4687
  }
3844
4688
  );
3845
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) => {
3846
- const raw = readFileSync7(tracePath, "utf-8");
4690
+ const raw = readFileSync8(tracePath, "utf-8");
3847
4691
  const trace = loadTrace(raw);
3848
4692
  const source = generateTest(trace, {
3849
4693
  description: opts.description,
@@ -3860,6 +4704,7 @@ function createProgram(options = {}) {
3860
4704
  const existingReportCmd = program2.commands.find((c) => c.name() === "report");
3861
4705
  if (existingReportCmd !== void 0) {
3862
4706
  registerBaselineSubCommand(existingReportCmd);
4707
+ registerDiffSubCommand(existingReportCmd);
3863
4708
  }
3864
4709
  return program2;
3865
4710
  }